glad-web 1.0.28 → 1.0.30
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/lib/claude/cli-usage.js +95 -0
- package/lib/claude/structured-session.js +223 -51
- package/lib/claude/transcript-repository.js +216 -0
- package/lib/codex/image-store.js +175 -0
- package/lib/codex/structured-session.js +18 -4
- package/lib/commands/web.js +52 -234
- package/lib/server/routes/providers.js +103 -0
- package/lib/server/routes/schedules.js +54 -0
- package/lib/server/routes/workspace.js +77 -0
- package/lib/session/session-manager.js +111 -340
- package/lib/web/claude.js +1074 -0
- package/lib/web/codex.js +475 -0
- package/lib/web/composer.js +230 -0
- package/lib/web/core.js +327 -0
- package/lib/web/git.js +533 -0
- package/lib/web/index.html +45 -3610
- package/lib/web/schedules.js +245 -0
- package/lib/web/session.js +351 -0
- package/lib/web/shell.js +56 -0
- package/lib/web/styles.css +377 -0
- package/lib/web/terminal-scroll.js +81 -0
- package/lib/web/timed-inputs.js +223 -0
- package/lib/workspace/service.js +3 -2
- package/package.json +10 -5
- package/scripts/check-syntax.js +26 -0
package/lib/web/index.html
CHANGED
|
@@ -8,347 +8,8 @@
|
|
|
8
8
|
<link rel="manifest" href="manifest.json">
|
|
9
9
|
<link rel="icon" type="image/svg+xml" href="logo.svg">
|
|
10
10
|
<link rel="apple-touch-icon" href="logo.svg">
|
|
11
|
-
<link rel="stylesheet" href="
|
|
12
|
-
<
|
|
13
|
-
:root { --primary: #007aff; --bg: #000; --card-bg: #1c1c1e; --text: #fff; --text-dim: #8e8e93; --terminal-controls-rest-height: 108px; --chat-content-max: 900px; --control-content-max: 1100px; }
|
|
14
|
-
body, html { margin: 0; padding: 0; height: 100dvh; width: 100vw; background: var(--bg); color: var(--text); overflow: hidden; overflow-anchor: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
15
|
-
.view { display: none; height: 100%; width: 100%; flex-direction: column; }
|
|
16
|
-
.view.active { display: flex; }
|
|
17
|
-
#lobby { overflow-y: auto; padding: 20px; padding-top: env(safe-area-inset-top); box-sizing: border-box; }
|
|
18
|
-
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
19
|
-
.header h1 { font-size: 28px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 10px; }
|
|
20
|
-
.header-logo { width: 32px; height: 32px; flex-shrink: 0; }
|
|
21
|
-
.btn-new { background: var(--primary); color: #fff; border: none; padding: 8px 11px; border-radius: 16px; font-weight: 700; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; min-height: 32px; white-space: nowrap; }
|
|
22
|
-
.btn-retry { background: #333; color: #fff; border: none; padding: 8px 16px; border-radius: 20px; margin-top: 10px; cursor: pointer; }
|
|
23
|
-
.session-card { background: var(--card-bg); border-radius: 12px; padding: 16px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; transition: transform 0.1s; position: relative; }
|
|
24
|
-
.session-card:active { transform: scale(0.98); }
|
|
25
|
-
.completion-dot { width: 9px; height: 9px; border-radius: 50%; background: #ff3b30; flex-shrink: 0; }
|
|
26
|
-
.session-info { flex: 1; min-width: 0; }
|
|
27
|
-
.session-info h3 { margin: 0 0 4px 0; font-size: 17px; display: flex; align-items: center; gap: 8px; }
|
|
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); }
|
|
34
|
-
.session-actions { display: flex; gap: 12px; align-items: center; margin-left: 10px; }
|
|
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; }
|
|
36
|
-
.icon-btn { color: var(--text-dim); background: none; border: none; padding: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
37
|
-
.icon-btn:active { color: var(--text); }
|
|
38
|
-
.btn-delete { color: #ff3b30; }
|
|
39
|
-
#modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: none; align-items: center; justify-content: center; padding: 20px; }
|
|
40
|
-
#tool-modal { background: var(--card-bg); width: 100%; max-width: 400px; border-radius: 16px; padding: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); }
|
|
41
|
-
.tool-item { padding: 12px; border-bottom: 1px solid #333; cursor: pointer; display: flex; align-items: center; border-radius: 8px; margin-top: 4px; }
|
|
42
|
-
.tool-item:hover { background: rgba(255,255,255,0.05); }
|
|
43
|
-
.tool-icon { width: 32px; height: 32px; background: #333; border-radius: 8px; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
|
44
|
-
#terminal-view { background: #000; overflow-anchor: none; position: relative; }
|
|
45
|
-
#top-ui { flex-shrink: 0; background: #121212; border-bottom: 1px solid #222; z-index: 2000; padding-top: env(safe-area-inset-top); position: relative; }
|
|
46
|
-
#nav-bar { display: flex; align-items: center; padding: 8px 14px; border-bottom: 1px solid #222; }
|
|
47
|
-
#back-btn { color: var(--primary); font-size: 16px; font-weight: 500; display: flex; align-items: center; text-decoration: none; background: none; border: none; padding: 0; cursor: pointer; flex-shrink: 0; }
|
|
48
|
-
#session-title { flex: 1; text-align: center; font-weight: 600; font-size: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 10px; min-width: 0; }
|
|
49
|
-
#terminal-controls { position: absolute; top: 100%; left: 0; right: 0; z-index: 2200; background: linear-gradient(180deg, rgba(18,18,18,0.98) 0%, rgba(18,18,18,0.92) 78%, rgba(18,18,18,0) 100%); border-bottom: 1px solid rgba(255,255,255,0.06); }
|
|
50
|
-
#terminal-controls-spacer { flex: 0 0 var(--terminal-controls-rest-height); height: var(--terminal-controls-rest-height); background: #000; }
|
|
51
|
-
#input-row { display: flex; align-items: flex-end; width: min(100%, var(--control-content-max)); margin: 0 auto; padding: 10px 14px 8px 14px; box-sizing: border-box; }
|
|
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; }
|
|
53
|
-
#cmd-input::placeholder { color: rgba(255,255,255,0.45); }
|
|
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; font-size: 24px; line-height: 1; }
|
|
56
|
-
#timer-btn.active { color: #fff; border-color: rgba(0,122,255,0.45); background: rgba(0,122,255,0.22); }
|
|
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
|
-
#composer-menu { display: none; width: min(calc(100% - 28px), var(--control-content-max)); margin: -2px auto 8px auto; padding: 6px; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(38,38,42,0.98); box-shadow: 0 12px 28px rgba(0,0,0,0.28); box-sizing: border-box; }
|
|
59
|
-
#composer-menu.active { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
|
|
60
|
-
.composer-menu-btn { min-height: 42px; border: 0; border-radius: 9px; background: rgba(255,255,255,0.08); color: #fff; font-size: 14px; font-weight: 650; cursor: pointer; }
|
|
61
|
-
.composer-menu-btn:hover { background: rgba(255,255,255,0.14); }
|
|
62
|
-
#attachment-strip { display: none; width: min(calc(100% - 28px), var(--control-content-max)); margin: -3px auto 2px auto; gap: 7px; overflow-x: auto; padding: 0 0 4px 0; box-sizing: border-box; scrollbar-width: none; }
|
|
63
|
-
#attachment-strip.active { display: flex; }
|
|
64
|
-
#attachment-strip::-webkit-scrollbar { display: none; }
|
|
65
|
-
.attachment-chip { min-width: 0; max-width: 210px; display: flex; align-items: center; gap: 7px; padding: 7px 9px; border: 1px solid rgba(0,122,255,0.45); border-radius: 10px; background: rgba(0,122,255,0.14); color: #e9f2ff; font-size: 12px; }
|
|
66
|
-
.attachment-chip.uploading { border-color: rgba(255,159,10,0.55); background: rgba(255,159,10,0.12); }
|
|
67
|
-
.attachment-chip-content { min-width: 0; flex: 1; }
|
|
68
|
-
.attachment-chip-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
69
|
-
.attachment-progress { display: block; height: 5px; margin-top: 5px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,0.16); }
|
|
70
|
-
.attachment-progress > span { display: block; height: 100%; border-radius: inherit; background: #ff9f0a; transition: width .15s ease; }
|
|
71
|
-
.attachment-progress.estimated > span { background: repeating-linear-gradient(90deg, #ff9f0a 0 12px, #ffd37b 12px 24px); background-size: 48px 100%; animation: attachment-upload-pulse .75s linear infinite; }
|
|
72
|
-
.attachment-status { display: block; margin-top: 3px; color: #ffd59a; font-size: 10px; }
|
|
73
|
-
@keyframes attachment-upload-pulse { from { background-position: 0 0; } to { background-position: 48px 0; } }
|
|
74
|
-
.attachment-remove { width: 19px; height: 19px; padding: 0; border: 0; border-radius: 50%; background: rgba(255,255,255,0.16); color: #fff; font-size: 15px; line-height: 18px; cursor: pointer; flex: 0 0 auto; }
|
|
75
|
-
#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; }
|
|
76
|
-
#timed-send-panel.active { display: block; }
|
|
77
|
-
.timed-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; align-items: center; }
|
|
78
|
-
.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; }
|
|
79
|
-
.timed-preview { margin-top: 8px; color: #d1d5db; font-size: 12px; line-height: 1.4; }
|
|
80
|
-
.timed-editor-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
|
|
81
|
-
#timed-tag-rail { position: absolute; left: 4px; top: 58px; display: flex; flex-direction: column; gap: 6px; z-index: 2400; pointer-events: none; }
|
|
82
|
-
.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; }
|
|
83
|
-
.timed-tag.active { background: rgba(0,122,255,0.42); border-color: rgba(255,255,255,0.28); }
|
|
84
|
-
.timed-tag:active { transform: scale(0.96); }
|
|
85
|
-
#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; }
|
|
86
|
-
#shortcut-rail::-webkit-scrollbar { display: none; }
|
|
87
|
-
.shortcut-group { flex: 0 0 calc(100vw - 28px); display: grid; gap: 8px; scroll-snap-align: start; }
|
|
88
|
-
.simple-shortcuts { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
89
|
-
.complex-shortcuts { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
90
|
-
.key-btn, .command-key { flex: 0 0 auto; min-width: 0; height: 40px; border: 1px solid rgba(255,255,255,0.11); border-radius: 12px; background: rgba(255,255,255,0.08); color: #d1d5db; font-size: 12px; font-weight: 700; letter-spacing: 0; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; white-space: nowrap; }
|
|
91
|
-
.key-btn { font-size: 11px; }
|
|
92
|
-
.shortcut-group .key-btn, .shortcut-group .command-key { width: 100%; }
|
|
93
|
-
.key-btn:active, .command-key:active { background: rgba(255,255,255,0.14); color: #fff; }
|
|
94
|
-
.key-btn.active { background: rgba(0, 122, 255, 0.25); border-color: #007aff; color: #00a2ff; }
|
|
95
|
-
#terminal-container { flex: 1; min-height: 0; width: 100%; background: #000; position: relative; overflow: hidden; overflow-anchor: none; }
|
|
96
|
-
#terminal { height: 100%; width: 100%; }
|
|
97
|
-
#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; }
|
|
98
|
-
#codex-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; }
|
|
99
|
-
.codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
|
|
100
|
-
.codex-working-indicator { position: sticky; top: 0; z-index: 4; width: 28px; height: 28px; margin: 0 0 -28px auto; border: 1px solid rgba(255,255,255,.1); border-radius: 50%; background: rgba(28,28,30,.68); box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(8px); pointer-events: none; }
|
|
101
|
-
.codex-working-indicator::after { content: ''; position: absolute; inset: 8px; border: 1.5px solid rgba(255,255,255,.7); border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; }
|
|
102
|
-
.codex-message-block { max-width: 100%; margin: 0 0 12px; }
|
|
103
|
-
.codex-message-block.user { max-width: 92%; margin-left: auto; }
|
|
104
|
-
.codex-message { max-width: 100%; margin: 0; color: #f5f5f7; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
105
|
-
.codex-message.user { padding: 8px 12px; border: 1px solid rgba(0,122,255,.32); border-radius: 12px; background: rgba(0,122,255,.24); }
|
|
106
|
-
.codex-message.event { color: var(--text-dim); text-align: center; font-size: 12px; }
|
|
107
|
-
.codex-message-time { display: block; width: max-content; margin-top: 4px; padding: 0 2px; color: #8e8e93; font-size: 10px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
108
|
-
.codex-message-block.user .codex-message-time { margin-left: auto; }
|
|
109
|
-
.codex-status-card { margin: 10px 0 14px; border: 1px solid rgba(100,210,255,.2); border-radius: 10px; background: rgba(28,28,30,.72); padding: 11px; color: #f5f5f7; }
|
|
110
|
-
.codex-status-title { margin-bottom: 9px; color: #64d2ff; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
111
|
-
.codex-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
|
112
|
-
.codex-status-item { min-width: 0; border: 1px solid rgba(255,255,255,.07); border-radius: 8px; background: rgba(255,255,255,.035); padding: 8px; }
|
|
113
|
-
.codex-status-label { color: #8e8e93; font-size: 10px; font-weight: 800; text-transform: uppercase; }
|
|
114
|
-
.codex-status-value { margin-top: 3px; color: #f5f5f7; font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
|
|
115
|
-
.codex-tool { border: 1px solid rgba(255,255,255,.1); border-radius: 8px; background: rgba(255,255,255,.045); overflow: hidden; margin: 8px 0; color: #d1d5db; }
|
|
116
|
-
.codex-tool summary { list-style: none; cursor: pointer; }
|
|
117
|
-
.codex-tool summary::-webkit-details-marker { display: none; }
|
|
118
|
-
.codex-tool-header { display: flex; align-items: center; min-width: 0; gap: 8px; min-height: 34px; padding: 6px 9px; }
|
|
119
|
-
.codex-tool-icon { width: 19px; flex: 0 0 19px; color: #c7c7cc; font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; }
|
|
120
|
-
.codex-tool-title { flex: 0 1 auto; min-width: 0; color: #f5f5f7; font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
121
|
-
.codex-tool-command { flex: 1; min-width: 0; color: #a9a9b0; font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
122
|
-
.codex-tool-state { flex: 0 0 auto; color: #8e8e93; font-size: 11px; }
|
|
123
|
-
.codex-tool-duration { flex: 0 0 auto; color: #6f6f76; font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
124
|
-
.codex-tool-state.running::before { content: ''; display: inline-block; width: 9px; height: 9px; margin-right: 5px; border: 1.5px solid #8e8e93; border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; vertical-align: -1px; }
|
|
125
|
-
.codex-tool.error { border-color: rgba(255,59,48,.38); background: rgba(255,59,48,.07); }
|
|
126
|
-
.codex-tool-body { border-top: 1px solid rgba(255,255,255,.08); padding: 9px 10px; }
|
|
127
|
-
.codex-tool-code { margin: 0; max-height: 320px; overflow: auto; color: #c7c7cc; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.42 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
128
|
-
.codex-work-group { margin: 8px 0; border-left: 1px solid rgba(255,255,255,.12); padding-left: 8px; }
|
|
129
|
-
.codex-work-group > summary { list-style: none; display: flex; align-items: center; gap: 7px; min-height: 30px; color: #a9a9b0; cursor: pointer; font-size: 12px; }
|
|
130
|
-
.codex-work-group > summary::-webkit-details-marker { display: none; }
|
|
131
|
-
.codex-work-group > summary::before { content: '›'; width: 12px; color: #8e8e93; font-size: 17px; transform-origin: center; }
|
|
132
|
-
.codex-work-group[open] > summary::before { transform: rotate(90deg); }
|
|
133
|
-
.codex-work-group-body { padding-left: 2px; }
|
|
134
|
-
.codex-patch-file { border-top: 1px solid rgba(255,255,255,.08); }
|
|
135
|
-
.codex-patch-file:first-child { border-top: 0; }
|
|
136
|
-
.codex-patch-file > summary { display: flex; align-items: center; gap: 8px; padding: 8px 10px; color: #d1d5db; font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
137
|
-
.codex-patch-file .path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
138
|
-
.codex-patch-kind { color: #8e8e93; font-family: system-ui, sans-serif; }
|
|
139
|
-
.codex-diff { overflow-x: auto; border-top: 1px solid rgba(255,255,255,.08); background: rgba(0,0,0,.24); font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
140
|
-
.codex-diff-line { min-width: max-content; padding: 0 10px; white-space: pre; color: #c7c7cc; }
|
|
141
|
-
.codex-diff-line.add { color: #b7e4c7; background: rgba(52,199,89,.1); }
|
|
142
|
-
.codex-diff-line.del { color: #ffb3ad; background: rgba(255,59,48,.1); }
|
|
143
|
-
.codex-diff-line.hunk { color: #64b5f6; background: rgba(0,122,255,.08); }
|
|
144
|
-
.codex-permission-result { color: #a9a9b0; font-size: 12px; margin-top: 7px; }
|
|
145
|
-
.codex-inline-permission { border-top: 1px solid rgba(255,204,0,.28); padding: 9px 10px 10px; background: rgba(255,204,0,.055); }
|
|
146
|
-
.codex-inline-permission-title { color: #f5f5f7; font-size: 12px; font-weight: 700; }
|
|
147
|
-
.codex-inline-permission .claude-permission-actions { margin-top: 8px; }
|
|
148
|
-
@keyframes codex-spin { to { transform: rotate(360deg); } }
|
|
149
|
-
#codex-control-panel { display: none; width: min(100%, var(--control-content-max)); margin: 0 auto; padding: 8px 14px 10px; border-bottom: 1px solid #222; background: #121212; box-sizing: border-box; }
|
|
150
|
-
.codex-control-rail { display: flex; gap: 14px; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
|
151
|
-
.codex-control-rail::-webkit-scrollbar { display: none; }
|
|
152
|
-
.codex-control-page { flex: 0 0 100%; display: grid; grid-template-columns: repeat(5, minmax(0, 116px)); grid-template-rows: auto; justify-content: center; gap: clamp(2px, .8vw, 7px); align-items: center; white-space: nowrap; scroll-snap-align: start; box-sizing: border-box; }
|
|
153
|
-
.codex-control-page > * { width: 100%; min-width: 0; overflow: hidden; }
|
|
154
|
-
.codex-control-page .claude-ctrl-btn, .codex-control-page .codex-select-label { padding-left: clamp(2px, 1vw, 10px); padding-right: clamp(2px, 1vw, 10px); font-size: clamp(8px, 2.2vw, 11px); text-overflow: ellipsis; overflow: hidden; }
|
|
155
|
-
.codex-select-control { position: relative; display: block; min-width: 0; height: 32px; }
|
|
156
|
-
.codex-select-label { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; box-sizing: border-box; border: 1px solid rgba(255,255,255,.1); border-radius: 16px; background: rgba(255,255,255,.08); color: #f5f5f7; font-size: 11px; font-weight: 800; white-space: nowrap; }
|
|
157
|
-
.codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
|
|
158
|
-
.codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
|
|
159
|
-
.codex-select option { background: #1c1c1e; color: #fff; }
|
|
160
|
-
#codex-model-panel, #codex-resume-panel, #codex-fork-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
|
|
161
|
-
#codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active { display: block; }
|
|
162
|
-
#codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
|
|
163
|
-
#codex-model-panel.active { display: grid; }
|
|
164
|
-
.codex-picker-column { min-width: 0; overflow-y: auto; }
|
|
165
|
-
.codex-picker-column + .codex-picker-column { border-left: 1px solid rgba(255,255,255,.08); }
|
|
166
|
-
.codex-picker-option { display: block; width: 100%; min-height: 38px; padding: 8px 10px; border: 0; border-bottom: 1px solid rgba(255,255,255,.06); background: transparent; color: #f5f5f7; text-align: left; font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
|
|
167
|
-
.codex-picker-option.selected { background: rgba(0,122,255,.18); color: #fff; }
|
|
168
|
-
#codex-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; }
|
|
169
|
-
#codex-state-bar::-webkit-scrollbar { display: none; }
|
|
170
|
-
#codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
|
|
171
|
-
.claude-context-size-badge { position: sticky; top: 0; z-index: 2; width: max-content; max-width: 100%; margin: 0 0 8px auto; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.94); border-radius: 999px; padding: 4px 9px; color: #d1d5db; font-size: 11px; font-weight: 800; box-shadow: 0 8px 18px rgba(0,0,0,0.24); }
|
|
172
|
-
.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; }
|
|
173
|
-
.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; }
|
|
174
|
-
.claude-message.assistant { max-width: 100%; background: transparent; border: 0; color: #f5f5f7; padding: 0 4px; margin: 0 0 12px 0; }
|
|
175
|
-
.claude-message.event { max-width: 100%; background: transparent; border: 0; color: var(--text-dim); font-size: 12px; padding: 3px 2px; text-align: center; }
|
|
176
|
-
.claude-message.error { color: #ff6b61; }
|
|
177
|
-
.claude-md { white-space: normal; }
|
|
178
|
-
.claude-md > :first-child { margin-top: 0; }
|
|
179
|
-
.claude-md > :last-child { margin-bottom: 0; }
|
|
180
|
-
.claude-md p { margin: 0 0 8px 0; }
|
|
181
|
-
.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; }
|
|
182
|
-
.claude-md h1 { font-size: 20px; }
|
|
183
|
-
.claude-md h2 { font-size: 18px; }
|
|
184
|
-
.claude-md h3 { font-size: 16px; }
|
|
185
|
-
.claude-md h4 { font-size: 15px; }
|
|
186
|
-
.claude-md ul, .claude-md ol { margin: 0 0 9px 0; padding-left: 20px; }
|
|
187
|
-
.claude-md li { margin: 4px 0; }
|
|
188
|
-
.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; }
|
|
189
|
-
.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; }
|
|
190
|
-
.claude-md pre code { display: block; padding: 11px 12px; background: transparent; border: 0; border-radius: 0; white-space: pre; line-height: 1.45; }
|
|
191
|
-
.claude-md blockquote { margin: 8px 0; padding: 2px 0 2px 10px; border-left: 3px solid rgba(255,255,255,0.22); color: #c7c7cc; }
|
|
192
|
-
.claude-md table { width: 100%; border-collapse: collapse; margin: 8px 0 10px 0; font-size: 13px; display: block; overflow-x: auto; }
|
|
193
|
-
.claude-md th, .claude-md td { border: 1px solid rgba(255,255,255,0.12); padding: 6px 8px; text-align: left; vertical-align: top; }
|
|
194
|
-
.claude-md th { background: rgba(255,255,255,0.07); color: #fff; }
|
|
195
|
-
.claude-md a { color: #64b5ff; text-decoration: none; }
|
|
196
|
-
.claude-md img { max-width: 100%; border-radius: 8px; margin: 6px 0; }
|
|
197
|
-
.claude-md hr { border: 0; border-top: 1px solid rgba(255,255,255,0.12); margin: 12px 0; }
|
|
198
|
-
.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; }
|
|
199
|
-
.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; }
|
|
200
|
-
.claude-tool strong { color: #fff; }
|
|
201
|
-
.claude-tool-card { display: block; }
|
|
202
|
-
.claude-tool-card summary { list-style: none; cursor: pointer; }
|
|
203
|
-
.claude-tool-card summary::-webkit-details-marker { display: none; }
|
|
204
|
-
.claude-tool-header { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 7px 9px; color: #f5f5f7; }
|
|
205
|
-
.claude-tool-icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-dim); flex: 0 0 auto; }
|
|
206
|
-
.claude-tool-icon::before { content: attr(data-icon); font-size: 11px; font-weight: 800; line-height: 1; }
|
|
207
|
-
.claude-tool-title { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
|
|
208
|
-
.claude-tool-status { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
|
|
209
|
-
.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; }
|
|
210
|
-
.claude-tool-body { padding: 0 10px 10px 10px; }
|
|
211
|
-
.claude-tool-section { margin-top: 8px; }
|
|
212
|
-
.claude-tool-section-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 5px; }
|
|
213
|
-
.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; }
|
|
214
|
-
.claude-tool.error { border-color: rgba(255,107,97,0.38); background: rgba(255,59,48,0.07); }
|
|
215
|
-
.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; }
|
|
216
|
-
.claude-work-group summary { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 7px 10px; cursor: pointer; color: #d1d5db; list-style: none; }
|
|
217
|
-
.claude-work-group summary::-webkit-details-marker { display: none; }
|
|
218
|
-
.claude-work-group-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
219
|
-
.claude-work-group-body { padding: 2px 8px 8px 8px; }
|
|
220
|
-
.claude-permission { border-color: rgba(255,204,0,0.4); background: rgba(255,204,0,0.08); }
|
|
221
|
-
.claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; flex-wrap: wrap; }
|
|
222
|
-
.claude-permission-actions .small-btn { min-height: 30px; max-width: 100%; white-space: normal; line-height: 1.2; }
|
|
223
|
-
.claude-status { color: var(--text-dim); font-size: 12px; text-align: center; padding: 6px 0 0 0; }
|
|
224
|
-
#claude-control-panel { display: none; padding: 0 14px 10px 14px; box-sizing: border-box; }
|
|
225
|
-
.claude-control-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 7px; }
|
|
226
|
-
.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 10px; outline: none; box-sizing: border-box; }
|
|
227
|
-
.claude-select { appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; }
|
|
228
|
-
.claude-picker-wrap { min-width: 0; position: relative; }
|
|
229
|
-
.claude-picker-wrap select { display: none; }
|
|
230
|
-
.claude-picker-btn { width: 100%; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
231
|
-
.claude-picker-btn.active { background: rgba(0,122,255,0.22); border-color: rgba(0,122,255,0.5); }
|
|
232
|
-
#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); }
|
|
233
|
-
#claude-picker-panel.active { display: block; }
|
|
234
|
-
#claude-usage-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; padding: 10px 11px; box-shadow: 0 16px 36px rgba(0,0,0,0.34); color: #f5f5f7; }
|
|
235
|
-
#claude-usage-panel.active { display: block; }
|
|
236
|
-
.claude-usage-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 9px; }
|
|
237
|
-
.claude-usage-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
238
|
-
.claude-usage-item { border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 8px; padding: 8px; min-width: 0; }
|
|
239
|
-
.claude-usage-label { color: var(--text-dim); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
240
|
-
.claude-usage-value { margin-top: 4px; color: #f5f5f7; font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
241
|
-
.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); }
|
|
242
|
-
.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; }
|
|
243
|
-
.claude-picker-option:last-child { border-bottom: 0; }
|
|
244
|
-
.claude-picker-option:active, .claude-picker-option.selected { background: rgba(0,122,255,0.18); }
|
|
245
|
-
.claude-picker-option-main { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; }
|
|
246
|
-
.claude-picker-option-main .selected-label { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; flex: 0 0 auto; }
|
|
247
|
-
.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; }
|
|
248
|
-
.claude-ctrl-btn { cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; }
|
|
249
|
-
.claude-ctrl-btn:active, .claude-select:focus { background: rgba(255,255,255,0.14); border-color: rgba(0,122,255,0.45); }
|
|
250
|
-
.claude-ctrl-btn.danger { color: #ff6b61; background: rgba(255,59,48,0.12); }
|
|
251
|
-
.claude-ctrl-btn.primary { color: #fff; background: rgba(0,122,255,0.32); border-color: rgba(0,122,255,0.45); }
|
|
252
|
-
.claude-ctrl-btn:disabled { opacity: 0.45; cursor: default; }
|
|
253
|
-
#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; }
|
|
254
|
-
#claude-state-bar::-webkit-scrollbar { display: none; }
|
|
255
|
-
.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; }
|
|
256
|
-
.claude-state-pill.warn { color: #ffcc00; border-color: rgba(255,204,0,0.24); background: rgba(255,204,0,0.08); }
|
|
257
|
-
.claude-state-pill.perm { margin-left: auto; }
|
|
258
|
-
.claude-state-pill.perm.acceptEdits { color: #34c759; border-color: rgba(52,199,89,0.26); background: rgba(52,199,89,0.08); }
|
|
259
|
-
.claude-state-pill.perm.bypassPermissions { color: #ff9f0a; border-color: rgba(255,159,10,0.26); background: rgba(255,159,10,0.08); }
|
|
260
|
-
.claude-state-pill.perm.plan { color: #64d2ff; border-color: rgba(100,210,255,0.26); background: rgba(100,210,255,0.08); }
|
|
261
|
-
#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; }
|
|
262
|
-
#claude-resume-panel.active { display: block; }
|
|
263
|
-
.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; }
|
|
264
|
-
.claude-resume-item:last-child { border-bottom: 0; }
|
|
265
|
-
.claude-resume-item:active { background: rgba(255,255,255,0.08); }
|
|
266
|
-
.claude-resume-title { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
|
|
267
|
-
.codex-resume-question-secondary { min-height: 1.35em; color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
|
|
268
|
-
.claude-resume-meta { color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
|
|
269
|
-
#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; }
|
|
270
|
-
#terminal .xterm-viewport { overflow-anchor: none; }
|
|
271
|
-
#terminal .xterm-helper-textarea { user-select: none !important; -webkit-user-select: none !important; -webkit-touch-callout: none; }
|
|
272
|
-
#scroll-controls { position: fixed; right: 14px; top: 60%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 18px; z-index: 3000; }
|
|
273
|
-
.scroll-btn { --charge: 0deg; width: 56px; height: 56px; background: rgba(30, 30, 30, 0.5); backdrop-filter: blur(15px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 28px; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); cursor: pointer; position: relative; overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none; }
|
|
274
|
-
.scroll-btn::before { content: ''; position: absolute; inset: 3px; border-radius: 50%; background: conic-gradient(var(--primary) var(--charge), transparent 0deg); opacity: 0; transition: opacity 0.12s ease; }
|
|
275
|
-
.scroll-btn::after { content: ''; position: absolute; inset: 7px; border-radius: 50%; background: rgba(30, 30, 30, 0.72); border: 1px solid rgba(255,255,255,0.08); }
|
|
276
|
-
.scroll-btn.charging::before { opacity: 1; }
|
|
277
|
-
.scroll-btn span { position: relative; z-index: 1; }
|
|
278
|
-
#debug-log { position: fixed; bottom: 0; left: 0; right: 0; max-height: 100px; background: rgba(0,0,0,0.8); color: #0f0; font-size: 10px; overflow-y: auto; padding: 5px; z-index: 9999; display: none; pointer-events: none; }
|
|
279
|
-
|
|
280
|
-
#history-content { flex: 1; background: #000; color: #d1d5db; padding: 14px; overflow-y: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; user-select: text !important; -webkit-user-select: text !important; box-sizing: border-box; }
|
|
281
|
-
#history-content.nowrap { white-space: pre; overflow-wrap: normal; overflow-x: auto; }
|
|
282
|
-
.history-meta { color: var(--text-dim); font-size: 11px; padding: 8px 14px; border-bottom: 1px solid rgba(255,255,255,0.06); background: #0d0d0d; }
|
|
283
|
-
|
|
284
|
-
.tab-bar { display: flex; border-bottom: 1px solid rgba(255,255,255,0.1); background: var(--bg); }
|
|
285
|
-
.tab-btn { flex: 1; text-align: center; padding: 12px 0; font-size: 14px; font-weight: 600; color: var(--text-dim); background: none; border: none; cursor: pointer; position: relative; }
|
|
286
|
-
.tab-btn.active { color: var(--text); }
|
|
287
|
-
.tab-btn.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); height: 2px; width: 40px; background: var(--primary); border-radius: 2px; }
|
|
288
|
-
.list-item { display: flex; align-items: center; padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer; transition: background 0.2s; }
|
|
289
|
-
.list-item:hover { background: rgba(255,255,255,0.05); }
|
|
290
|
-
.dir-icon { color: var(--primary); margin-right: 12px; }
|
|
291
|
-
.file-icon { color: var(--text-dim); margin-right: 12px; }
|
|
292
|
-
.btn-toggle { background: var(--card-bg); border-radius: 6px; padding: 4px; display: flex; gap: 4px; margin: 10px; }
|
|
293
|
-
.btn-toggle button { flex: 1; padding: 6px 0; border-radius: 4px; border: none; background: transparent; color: var(--text-dim); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
294
|
-
.btn-toggle button.active { background: var(--primary); color: #fff; }
|
|
295
|
-
.lobby-tabs { display: flex; background: var(--card-bg); border-radius: 8px; padding: 4px; margin-bottom: 16px; gap: 4px; }
|
|
296
|
-
.lobby-tab { flex: 1; border: none; border-radius: 6px; background: transparent; color: var(--text-dim); padding: 8px; font-size: 14px; font-weight: 700; cursor: pointer; }
|
|
297
|
-
.lobby-tab.active { background: var(--primary); color: #fff; }
|
|
298
|
-
.schedule-card { background: var(--card-bg); border-radius: 12px; padding: 16px; margin-bottom: 12px; }
|
|
299
|
-
.schedule-row { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; }
|
|
300
|
-
.schedule-meta { margin-top: 6px; color: var(--text-dim); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
|
|
301
|
-
.schedule-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
|
302
|
-
.small-btn { border: none; border-radius: 16px; padding: 7px 10px; font-weight: 700; font-size: 12px; color: #fff; background: rgba(255,255,255,0.1); cursor: pointer; }
|
|
303
|
-
.small-btn.primary { background: var(--primary); }
|
|
304
|
-
.small-btn.danger { background: rgba(255,59,48,0.18); color: #ff6b61; }
|
|
305
|
-
.form-field { margin-bottom: 12px; }
|
|
306
|
-
.form-field label { display:block; margin-bottom: 6px; font-size: 13px; color: var(--text-dim); font-weight: 600; }
|
|
307
|
-
.form-field input, .form-field select, .form-field textarea { width: 100%; box-sizing: border-box; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); color: #fff; border-radius: 8px; padding: 9px 10px; font-size: 14px; outline: none; }
|
|
308
|
-
.weekday-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
|
|
309
|
-
.weekday-btn { border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.05); color: var(--text-dim); border-radius: 8px; padding: 8px 0; font-size: 12px; font-weight: 700; cursor: pointer; }
|
|
310
|
-
.weekday-btn.active { border-color: var(--primary); background: rgba(0,122,255,0.25); color: #fff; }
|
|
311
|
-
#schedule-modal { background: var(--card-bg); width: 100%; max-width: 720px; max-height: 92dvh; overflow-y: auto; border-radius: 16px; padding: 18px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); box-sizing: border-box; }
|
|
312
|
-
.step-card { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 10px; margin-bottom: 8px; background: rgba(255,255,255,0.04); }
|
|
313
|
-
.step-grid { display: grid; grid-template-columns: minmax(110px, 150px) 1fr auto; gap: 8px; align-items: center; }
|
|
314
|
-
@media (max-width: 640px) {
|
|
315
|
-
.schedule-row { flex-direction: column; }
|
|
316
|
-
.schedule-actions { justify-content: flex-start; }
|
|
317
|
-
.step-grid { grid-template-columns: 1fr; }
|
|
318
|
-
.timed-row { grid-template-columns: 1fr; }
|
|
319
|
-
}
|
|
320
|
-
@media (max-width: 480px) {
|
|
321
|
-
:root { --chat-content-max: 100%; --control-content-max: 100%; }
|
|
322
|
-
#nav-bar { padding: 8px 10px; }
|
|
323
|
-
#back-btn { font-size: 15px; }
|
|
324
|
-
#back-btn svg { width: 22px; height: 22px; }
|
|
325
|
-
#session-title { font-size: 15px; padding: 0 7px; }
|
|
326
|
-
#nav-bar > div:last-child { gap: 5px !important; }
|
|
327
|
-
#nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
|
|
328
|
-
#input-row { padding-left: 10px; padding-right: 10px; }
|
|
329
|
-
#codex-control-panel { padding-left: 8px; padding-right: 8px; }
|
|
330
|
-
.codex-control-page { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
|
331
|
-
.codex-control-page .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
|
|
332
|
-
.codex-select-control { height: 36px; }
|
|
333
|
-
#codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
|
|
334
|
-
.codex-message-block.user { max-width: 94%; }
|
|
335
|
-
.claude-permission-actions { justify-content: flex-start; }
|
|
336
|
-
}
|
|
337
|
-
@media (max-width: 430px) {
|
|
338
|
-
#nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) { width: 34px; font-size: 0 !important; gap: 0 !important; }
|
|
339
|
-
#nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) svg { width: 16px; height: 16px; }
|
|
340
|
-
#codex-terminal-switch { padding-left: 8px !important; padding-right: 8px !important; }
|
|
341
|
-
}
|
|
342
|
-
@media (min-width: 768px) and (max-width: 1199px) {
|
|
343
|
-
:root { --chat-content-max: 780px; --control-content-max: 920px; }
|
|
344
|
-
#codex-chat-container { padding: 16px 18px 30px; }
|
|
345
|
-
#input-row, #codex-control-panel { padding-left: 18px; padding-right: 18px; }
|
|
346
|
-
}
|
|
347
|
-
@media (min-width: 1200px) {
|
|
348
|
-
:root { --chat-content-max: 960px; --control-content-max: 1100px; }
|
|
349
|
-
#codex-chat-container { padding: 18px 22px 34px; }
|
|
350
|
-
}
|
|
351
|
-
</style>
|
|
11
|
+
<link rel="stylesheet" href="vendor/xterm.css" />
|
|
12
|
+
<link rel="stylesheet" href="styles.css">
|
|
352
13
|
</head>
|
|
353
14
|
<body>
|
|
354
15
|
<div id="debug-log"></div>
|
|
@@ -413,40 +74,43 @@
|
|
|
413
74
|
</div>
|
|
414
75
|
<input id="image-file-input" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple hidden>
|
|
415
76
|
<div id="claude-control-panel">
|
|
416
|
-
<div class="claude-control-
|
|
417
|
-
<div class="claude-
|
|
418
|
-
<
|
|
419
|
-
<
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
<
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
77
|
+
<div class="claude-control-rail">
|
|
78
|
+
<div class="claude-control-page primary">
|
|
79
|
+
<div class="claude-picker-wrap">
|
|
80
|
+
<select id="claude-model-select" class="claude-select" title="Model" onchange="updateClaudeSettingsFromControls()">
|
|
81
|
+
<option value="default">M:Def</option>
|
|
82
|
+
</select>
|
|
83
|
+
<select id="claude-effort-select" class="claude-select" title="Effort" onchange="updateClaudeSettingsFromControls()">
|
|
84
|
+
<option value="low">Low</option>
|
|
85
|
+
<option value="medium">Medium</option>
|
|
86
|
+
<option value="high">High</option>
|
|
87
|
+
<option value="xhigh">Extra high</option>
|
|
88
|
+
<option value="max">Max</option>
|
|
89
|
+
</select>
|
|
90
|
+
<button id="claude-model-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('model')" title="Model and effort">Model</button>
|
|
91
|
+
</div>
|
|
92
|
+
<button id="claude-usage-btn" class="claude-ctrl-btn" onclick="requestClaudeUsage()" title="Show Claude usage">Usage</button>
|
|
93
|
+
<button id="claude-context-btn" class="claude-ctrl-btn" onclick="requestClaudeContext()" title="Show Claude context">Context</button>
|
|
94
|
+
<button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
|
|
95
|
+
<button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
|
|
96
|
+
<button id="claude-fork-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeForkPanel()" title="Fork a Claude session">Fork</button>
|
|
431
97
|
</div>
|
|
432
|
-
<div class="claude-
|
|
433
|
-
<
|
|
434
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
98
|
+
<div class="claude-control-page secondary">
|
|
99
|
+
<div class="claude-picker-wrap">
|
|
100
|
+
<select id="claude-permission-select" class="claude-select" title="Permission mode" onchange="updateClaudeSettingsFromControls()">
|
|
101
|
+
<option value="default">Default</option>
|
|
102
|
+
<option value="acceptEdits">Accept edits</option>
|
|
103
|
+
<option value="plan">Plan mode</option>
|
|
104
|
+
<option value="bypassPermissions">Bypass</option>
|
|
105
|
+
</select>
|
|
106
|
+
<button id="claude-permission-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('permission')" title="Permission mode">Permission</button>
|
|
107
|
+
</div>
|
|
441
108
|
</div>
|
|
442
|
-
<button id="claude-usage-btn" class="claude-ctrl-btn" onclick="toggleClaudeUsagePanel()" title="Usage">Usage</button>
|
|
443
|
-
<button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
|
|
444
|
-
<button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
|
|
445
109
|
</div>
|
|
446
110
|
<div id="claude-state-bar"></div>
|
|
447
111
|
<div id="claude-picker-panel"></div>
|
|
448
|
-
<div id="claude-usage-panel"></div>
|
|
449
112
|
<div id="claude-resume-panel"></div>
|
|
113
|
+
<div id="claude-fork-panel"></div>
|
|
450
114
|
</div>
|
|
451
115
|
<div id="codex-control-panel">
|
|
452
116
|
<div id="codex-control-rail" class="codex-control-rail">
|
|
@@ -631,3247 +295,18 @@
|
|
|
631
295
|
</div>
|
|
632
296
|
</div>
|
|
633
297
|
|
|
634
|
-
<script defer src="
|
|
635
|
-
<script defer src="
|
|
298
|
+
<script defer src="vendor/xterm.js"></script>
|
|
299
|
+
<script defer src="vendor/xterm-addon-fit.js"></script>
|
|
636
300
|
<script src="gitgraph.js"></script>
|
|
637
|
-
<script>
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
let timedTagTimer = null;
|
|
648
|
-
let editingTimedInputId = null;
|
|
649
|
-
let claudeMessages = [];
|
|
650
|
-
let claudePendingPermissions = [];
|
|
651
|
-
let claudeStatus = 'idle';
|
|
652
|
-
let claudeRuntimeConfig = null;
|
|
653
|
-
let claudePickerOpen = null;
|
|
654
|
-
let claudeUsagePanelOpen = false;
|
|
655
|
-
const CLAUDE_CONTEXT_SIZE = 190000;
|
|
656
|
-
let claudeState = {
|
|
657
|
-
permissionMode: 'default',
|
|
658
|
-
model: 'default',
|
|
659
|
-
effort: 'medium',
|
|
660
|
-
contextRemaining: null,
|
|
661
|
-
latestUsage: null,
|
|
662
|
-
claudeSessionId: null,
|
|
663
|
-
resumeSessionId: null,
|
|
664
|
-
canAbort: false,
|
|
665
|
-
pendingPermissionCount: 0
|
|
666
|
-
};
|
|
667
|
-
let claudeResumePanelOpen = false;
|
|
668
|
-
let claudeResumeItemsLoaded = false;
|
|
669
|
-
let codexMessages = [];
|
|
670
|
-
let codexPendingPermissions = [];
|
|
671
|
-
let codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
672
|
-
let codexModelPanelOpen = false;
|
|
673
|
-
let codexModelCandidate = null;
|
|
674
|
-
let codexResumePanelOpen = false;
|
|
675
|
-
let codexForkPanelOpen = false;
|
|
676
|
-
let codexRenderFrame = null;
|
|
677
|
-
const modifiers = { ctrl: false };
|
|
678
|
-
|
|
679
|
-
function log(msg) {
|
|
680
|
-
const el = document.getElementById('debug-log');
|
|
681
|
-
const entry = document.createElement('div');
|
|
682
|
-
entry.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
|
|
683
|
-
el.appendChild(entry);
|
|
684
|
-
console.log(msg);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
|
|
688
|
-
const controller = new AbortController();
|
|
689
|
-
const id = setTimeout(() => controller.abort(), timeout);
|
|
690
|
-
try {
|
|
691
|
-
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
692
|
-
clearTimeout(id);
|
|
693
|
-
return response;
|
|
694
|
-
} catch (e) {
|
|
695
|
-
clearTimeout(id);
|
|
696
|
-
if (e && e.name === 'AbortError') {
|
|
697
|
-
throw new Error(`Request timed out after ${Math.round(timeout / 1000)}s`);
|
|
698
|
-
}
|
|
699
|
-
throw e;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function escapeHtml(value = '') {
|
|
704
|
-
return String(value)
|
|
705
|
-
.replace(/&/g, '&')
|
|
706
|
-
.replace(/</g, '<')
|
|
707
|
-
.replace(/>/g, '>')
|
|
708
|
-
.replace(/"/g, '"')
|
|
709
|
-
.replace(/'/g, ''');
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function encodePathValue(path = '') {
|
|
713
|
-
return encodeURIComponent(path).replace(/'/g, '%27');
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function decodePathValue(path = '') {
|
|
717
|
-
return decodeURIComponent(path);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
async function copyTextToClipboard(text) {
|
|
721
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
722
|
-
await navigator.clipboard.writeText(text);
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
const el = document.createElement('textarea');
|
|
726
|
-
el.value = text;
|
|
727
|
-
el.setAttribute('readonly', '');
|
|
728
|
-
el.style.position = 'fixed';
|
|
729
|
-
el.style.opacity = '0';
|
|
730
|
-
document.body.appendChild(el);
|
|
731
|
-
el.select();
|
|
732
|
-
document.execCommand('copy');
|
|
733
|
-
document.body.removeChild(el);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
async function copySessionDirectory(directory, event) {
|
|
737
|
-
event.stopPropagation();
|
|
738
|
-
const btn = event.currentTarget;
|
|
739
|
-
try {
|
|
740
|
-
await copyTextToClipboard(directory);
|
|
741
|
-
const oldTitle = btn.title;
|
|
742
|
-
btn.title = 'Copied';
|
|
743
|
-
btn.style.color = '#fff';
|
|
744
|
-
setTimeout(() => {
|
|
745
|
-
btn.title = oldTitle || 'Copy directory';
|
|
746
|
-
btn.style.color = '';
|
|
747
|
-
}, 1200);
|
|
748
|
-
} catch (e) {
|
|
749
|
-
alert('Copy failed');
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function formatWeekdays(days = []) {
|
|
754
|
-
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
755
|
-
return days.map(day => labels[day]).filter(Boolean).join(', ') || 'No days';
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function formatDateTime(value) {
|
|
759
|
-
return value ? new Date(value).toLocaleString() : 'Not scheduled';
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function switchLobbyTab(tab) {
|
|
763
|
-
lobbyTab = tab;
|
|
764
|
-
document.getElementById('lobby-tab-sessions').classList.toggle('active', tab === 'sessions');
|
|
765
|
-
document.getElementById('lobby-tab-schedules').classList.toggle('active', tab === 'schedules');
|
|
766
|
-
document.getElementById('sessions-list').style.display = tab === 'sessions' ? '' : 'none';
|
|
767
|
-
document.getElementById('schedules-list').style.display = tab === 'schedules' ? '' : 'none';
|
|
768
|
-
if (tab === 'sessions') refreshSessionsNow();
|
|
769
|
-
if (tab === 'schedules') loadSchedules();
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
async function loadSchedules() {
|
|
773
|
-
const list = document.getElementById('schedules-list');
|
|
774
|
-
list.innerHTML = '<p style="color:#888; text-align:center; margin-top:50px;">Loading schedules...</p>';
|
|
775
|
-
try {
|
|
776
|
-
await ensureScheduleTools().catch(() => {});
|
|
777
|
-
const res = await fetchWithTimeout('/api/schedules');
|
|
778
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
779
|
-
const schedules = await res.json();
|
|
780
|
-
if (!schedules.length) {
|
|
781
|
-
list.innerHTML = '<p style="color:#888; text-align:center; margin-top:50px;">No scheduled tasks</p>';
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
let html = '';
|
|
786
|
-
schedules.forEach(job => {
|
|
787
|
-
const encodedId = encodePathValue(job.id);
|
|
788
|
-
const tool = scheduleTools.find(t => t.key === job.target.toolKey);
|
|
789
|
-
const statusColor = job.lastRunStatus === 'failed' ? '#ff6b61' : job.running ? '#5ac8fa' : 'var(--text-dim)';
|
|
790
|
-
html += `<div class="schedule-card">
|
|
791
|
-
<div class="schedule-row">
|
|
792
|
-
<div style="flex:1; min-width:0;">
|
|
793
|
-
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
|
|
794
|
-
<strong>${escapeHtml(job.name)}</strong>
|
|
795
|
-
<span style="font-size:11px; color:${job.enabled ? '#34c759' : '#8e8e93'}; border:1px solid currentColor; border-radius:10px; padding:1px 7px;">${job.enabled ? 'ON' : 'OFF'}</span>
|
|
796
|
-
</div>
|
|
797
|
-
<div class="schedule-meta">
|
|
798
|
-
${escapeHtml(job.schedule.time)} | ${escapeHtml(formatWeekdays(job.schedule.weekdays))}<br>
|
|
799
|
-
${escapeHtml(tool ? tool.displayName : job.target.toolKey)} | ${escapeHtml(job.target.workingDirectory || 'Default directory')}<br>
|
|
800
|
-
Next: ${escapeHtml(formatDateTime(job.nextRunAt))}<br>
|
|
801
|
-
<span style="color:${statusColor};">Last: ${escapeHtml(job.lastRunStatus || 'idle')}${job.lastRunMessage ? ' - ' + escapeHtml(job.lastRunMessage) : ''}</span>
|
|
802
|
-
</div>
|
|
803
|
-
</div>
|
|
804
|
-
<div class="schedule-actions">
|
|
805
|
-
<button class="small-btn primary" onclick="simulateSchedule(decodePathValue('${encodedId}'))">Test</button>
|
|
806
|
-
<button class="small-btn" onclick="editSchedule(decodePathValue('${encodedId}'))">Edit</button>
|
|
807
|
-
<button class="small-btn" onclick="duplicateSchedule(decodePathValue('${encodedId}'))">Copy</button>
|
|
808
|
-
<button class="small-btn" onclick="toggleSchedule(decodePathValue('${encodedId}'), ${job.enabled ? 'false' : 'true'})">${job.enabled ? 'Disable' : 'Enable'}</button>
|
|
809
|
-
<button class="small-btn danger" onclick="deleteSchedule(decodePathValue('${encodedId}'))">Delete</button>
|
|
810
|
-
</div>
|
|
811
|
-
</div>
|
|
812
|
-
</div>`;
|
|
813
|
-
});
|
|
814
|
-
list.innerHTML = html;
|
|
815
|
-
} catch (e) {
|
|
816
|
-
list.innerHTML = `<div style="color:#ff3b30; text-align:center; margin-top:50px;"><p>Failed to load: ${escapeHtml(e.message)}</p><button class="btn-retry" onclick="loadSchedules()">Retry</button></div>`;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
async function loadSessions() {
|
|
821
|
-
log('Loading sessions...');
|
|
822
|
-
const list = document.getElementById('sessions-list');
|
|
823
|
-
try {
|
|
824
|
-
const res = await fetchWithTimeout('/api/sessions');
|
|
825
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
826
|
-
const sessions = await res.json();
|
|
827
|
-
if (!sessions || sessions.length === 0) {
|
|
828
|
-
list.innerHTML = '<p style="color:#888; text-align:center; margin-top:50px;">No active sessions</p>';
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
let html = '';
|
|
833
|
-
sessions.forEach(s => {
|
|
834
|
-
const workingDirectory = s.workingDirectory || 'Unknown directory';
|
|
835
|
-
const encodedName = encodePathValue(s.name);
|
|
836
|
-
const encodedDir = encodePathValue(workingDirectory);
|
|
837
|
-
const timedInputCount = Number(s.timedInputCount) || 0;
|
|
838
|
-
const timerBadge = timedInputCount > 0
|
|
839
|
-
? `<span class="timer-count-badge" title="${timedInputCount} scheduled timer${timedInputCount > 1 ? 's' : ''}">
|
|
840
|
-
<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>
|
|
841
|
-
${timedInputCount}
|
|
842
|
-
</span>`
|
|
843
|
-
: '';
|
|
844
|
-
html += `<div class="session-card">
|
|
845
|
-
<div class="session-info">
|
|
846
|
-
<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>
|
|
847
|
-
<p>${escapeHtml(s.tool)}</p>
|
|
848
|
-
<p>${new Date(s.startTime).toLocaleTimeString()}</p>
|
|
849
|
-
<div class="session-dir-row">
|
|
850
|
-
<button class="copy-dir-btn" title="Copy directory" onclick="copySessionDirectory(decodePathValue('${encodedDir}'), event)">
|
|
851
|
-
<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>
|
|
852
|
-
</button>
|
|
853
|
-
<p title="${escapeHtml(workingDirectory)}">${escapeHtml(workingDirectory)}</p>
|
|
854
|
-
</div>
|
|
855
|
-
</div>
|
|
856
|
-
<div class="session-actions">
|
|
857
|
-
<button class="btn-join" onclick="joinSession('${s.id}', decodePathValue('${encodedName}'), '${escapeHtml(s.toolKey || '')}')">Connect</button>
|
|
858
|
-
<button class="icon-btn btn-delete" onclick="deleteSession('${s.id}', event)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>
|
|
859
|
-
</div>
|
|
860
|
-
</div>`;
|
|
861
|
-
});
|
|
862
|
-
list.innerHTML = html;
|
|
863
|
-
} catch (e) {
|
|
864
|
-
list.innerHTML = `<div style="color:#ff3b30; text-align:center; margin-top:50px;"><p>Failed to load: ${e.message}</p><button class="btn-retry" onclick="loadSessions()">Retry</button></div>`;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
async function renameSession(id, oldName, e) {
|
|
869
|
-
e.stopPropagation();
|
|
870
|
-
const newName = prompt('Rename session', oldName);
|
|
871
|
-
if (newName && newName !== oldName) {
|
|
872
|
-
try {
|
|
873
|
-
await fetchWithTimeout('/api/sessions/' + id, {
|
|
874
|
-
method: 'PATCH',
|
|
875
|
-
headers: { 'Content-Type': 'application/json' },
|
|
876
|
-
body: JSON.stringify({ name: newName })
|
|
877
|
-
});
|
|
878
|
-
refreshSessionsNow();
|
|
879
|
-
} catch (e) { alert('Rename failed'); }
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
async function showToolModal() {
|
|
884
|
-
document.getElementById('modal-overlay').style.display = 'flex';
|
|
885
|
-
const list = document.getElementById('tools-list');
|
|
886
|
-
loadAppConfig();
|
|
887
|
-
try {
|
|
888
|
-
const res = await fetchWithTimeout('/api/tools');
|
|
889
|
-
const tools = await res.json();
|
|
890
|
-
|
|
891
|
-
let html = '';
|
|
892
|
-
tools.forEach(t => {
|
|
893
|
-
const versionLabel = t.version && t.version !== 'unknown' ? `v${t.version}` : 'version unknown';
|
|
894
|
-
html += `<div class="tool-item" onclick='createSession(${JSON.stringify(t.key)}, ${JSON.stringify(t.displayName)})'>
|
|
895
|
-
<div class="tool-icon">${t.displayName[0]}</div>
|
|
896
|
-
<div>
|
|
897
|
-
<div style="font-weight:600">${t.displayName}</div>
|
|
898
|
-
<div style="font-size:12px; color:#888">${versionLabel}</div>
|
|
899
|
-
</div>
|
|
900
|
-
</div>`;
|
|
901
|
-
});
|
|
902
|
-
list.innerHTML = html;
|
|
903
|
-
} catch (e) {
|
|
904
|
-
list.innerHTML = `<p style="color:#ff3b30">Detection failed: ${e.message}</p>`;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
async function loadAppConfig() {
|
|
909
|
-
if (appConfig) {
|
|
910
|
-
document.getElementById('default-cwd').textContent = appConfig.defaultWorkingDirectory || 'Current folder';
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
try {
|
|
914
|
-
const res = await fetchWithTimeout('/api/config');
|
|
915
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
916
|
-
appConfig = await res.json();
|
|
917
|
-
document.getElementById('default-cwd').textContent = appConfig.defaultWorkingDirectory || 'Current folder';
|
|
918
|
-
} catch (e) {
|
|
919
|
-
document.getElementById('default-cwd').textContent = 'Current folder';
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function closeToolModal(e) {
|
|
924
|
-
if (e.target.id === 'modal-overlay') document.getElementById('modal-overlay').style.display = 'none';
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function isLobbyVisible() {
|
|
928
|
-
return document.getElementById('lobby-view').classList.contains('active');
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function scheduleSessionPolling() {
|
|
932
|
-
clearTimeout(sessionPollTimer);
|
|
933
|
-
sessionPollTimer = null;
|
|
934
|
-
if (!isLobbyVisible()) return;
|
|
935
|
-
|
|
936
|
-
const delay = document.hidden ? 30000 : 10000;
|
|
937
|
-
sessionPollTimer = setTimeout(async () => {
|
|
938
|
-
await loadSessions();
|
|
939
|
-
scheduleSessionPolling();
|
|
940
|
-
}, delay);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
async function refreshSessionsNow() {
|
|
944
|
-
clearTimeout(sessionPollTimer);
|
|
945
|
-
sessionPollTimer = null;
|
|
946
|
-
if (!isLobbyVisible()) return;
|
|
947
|
-
if (lobbyTab === 'schedules') {
|
|
948
|
-
await loadSchedules();
|
|
949
|
-
scheduleSessionPolling();
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
await loadSessions();
|
|
953
|
-
scheduleSessionPolling();
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
async function markCompletionRead(id) {
|
|
957
|
-
try {
|
|
958
|
-
await fetchWithTimeout('/api/sessions/' + id + '/completion/read', { method: 'POST' });
|
|
959
|
-
} catch (e) {
|
|
960
|
-
log('Failed to mark completion read: ' + e.message);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function isClaudeSession() {
|
|
965
|
-
return activeToolKey === 'claude-code';
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function isCodexSession() {
|
|
969
|
-
return activeToolKey === 'codex';
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function setClaudeModeEnabled(enabled) {
|
|
973
|
-
const codexChat = isCodexSession() && codexState.presentation === 'structured';
|
|
974
|
-
const structured = enabled || codexChat;
|
|
975
|
-
document.getElementById('terminal-container').style.display = structured ? 'none' : '';
|
|
976
|
-
document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
|
|
977
|
-
document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
|
|
978
|
-
document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
|
|
979
|
-
document.getElementById('codex-control-panel').style.display = codexChat ? 'block' : 'none';
|
|
980
|
-
document.getElementById('codex-terminal-switch').style.display = isCodexSession() ? '' : 'none';
|
|
981
|
-
document.getElementById('timer-btn').style.display = '';
|
|
982
|
-
document.getElementById('attach-image-btn').style.display = codexChat ? '' : 'none';
|
|
983
|
-
document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
|
|
984
|
-
document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
|
|
985
|
-
document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : codexChat ? 'Message Codex...' : 'Type a message...';
|
|
986
|
-
updateTerminalControlsHeight();
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function updateTerminalControlsHeight() {
|
|
990
|
-
const controls = document.getElementById('terminal-controls');
|
|
991
|
-
const measuredHeight = controls ? Math.ceil(controls.getBoundingClientRect().height) : 0;
|
|
992
|
-
document.documentElement.style.setProperty('--terminal-controls-rest-height', `${Math.max(0, measuredHeight)}px`);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function shortModelLabel(value, resolved) {
|
|
996
|
-
const source = String(value || 'default');
|
|
997
|
-
const target = String(resolved || source);
|
|
998
|
-
const compact = target
|
|
999
|
-
.replace(/^claude-/, '')
|
|
1000
|
-
.replace(/-20\d{6,}.*$/, '')
|
|
1001
|
-
.slice(0, 9);
|
|
1002
|
-
if (source === 'default') return 'M:Def';
|
|
1003
|
-
if (source === 'env') return `M:${compact || 'Env'}`;
|
|
1004
|
-
if (source === 'sonnet') return `M:${compact || 'Son'}`;
|
|
1005
|
-
if (source === 'opus') return `M:${compact || 'Opus'}`;
|
|
1006
|
-
if (source === 'haiku') return `M:${compact || 'Hai'}`;
|
|
1007
|
-
return `M:${compact || source.slice(0, 9)}`;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function fullModelLabel(value, resolved, label) {
|
|
1011
|
-
const source = String(value || 'default');
|
|
1012
|
-
if (source === 'default') return 'Default';
|
|
1013
|
-
if (source === 'env') return resolved ? `Environment (${resolved})` : 'Environment';
|
|
1014
|
-
if (label && label !== value) return label;
|
|
1015
|
-
return String(resolved || value || 'Model');
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function permissionModeLabel(value) {
|
|
1019
|
-
const labels = {
|
|
1020
|
-
default: 'Default',
|
|
1021
|
-
acceptEdits: 'Accept edits',
|
|
1022
|
-
plan: 'Plan mode',
|
|
1023
|
-
bypassPermissions: 'Bypass'
|
|
1024
|
-
};
|
|
1025
|
-
return labels[value] || String(value || 'Default');
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function effortLabel(value) {
|
|
1029
|
-
const labels = {
|
|
1030
|
-
low: 'Low',
|
|
1031
|
-
medium: 'Medium',
|
|
1032
|
-
high: 'High',
|
|
1033
|
-
xhigh: 'Extra high',
|
|
1034
|
-
max: 'Max'
|
|
1035
|
-
};
|
|
1036
|
-
return labels[value] || String(value || 'Medium');
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
function pickerLabelFromOption(option) {
|
|
1040
|
-
return option ? (option.textContent || option.value || '') : '';
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function pickerFullValue(option) {
|
|
1044
|
-
if (!option) return '';
|
|
1045
|
-
return option.dataset.resolved || '';
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function syncClaudePickerButtons() {
|
|
1049
|
-
const mappings = [
|
|
1050
|
-
['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'Perm'],
|
|
1051
|
-
['model', 'claude-model-select', 'claude-model-picker-btn', 'Model'],
|
|
1052
|
-
['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'Effort']
|
|
1053
|
-
];
|
|
1054
|
-
mappings.forEach(([type, selectId, buttonId, prefix]) => {
|
|
1055
|
-
const select = document.getElementById(selectId);
|
|
1056
|
-
const button = document.getElementById(buttonId);
|
|
1057
|
-
if (!select || !button) return;
|
|
1058
|
-
const option = select.selectedOptions && select.selectedOptions[0];
|
|
1059
|
-
const label = pickerLabelFromOption(option);
|
|
1060
|
-
const fullValue = pickerFullValue(option);
|
|
1061
|
-
button.textContent = prefix;
|
|
1062
|
-
button.title = fullValue || label || prefix;
|
|
1063
|
-
button.classList.toggle('active', claudePickerOpen === type);
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function closeClaudePicker() {
|
|
1068
|
-
claudePickerOpen = null;
|
|
1069
|
-
const panel = document.getElementById('claude-picker-panel');
|
|
1070
|
-
if (panel) {
|
|
1071
|
-
panel.classList.remove('active');
|
|
1072
|
-
panel.innerHTML = '';
|
|
1073
|
-
}
|
|
1074
|
-
syncClaudePickerButtons();
|
|
1075
|
-
updateTerminalControlsHeight();
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function closeClaudeUsagePanel() {
|
|
1079
|
-
claudeUsagePanelOpen = false;
|
|
1080
|
-
const panel = document.getElementById('claude-usage-panel');
|
|
1081
|
-
if (panel) panel.classList.remove('active');
|
|
1082
|
-
updateTerminalControlsHeight();
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function pickerTitle(type) {
|
|
1086
|
-
if (type === 'permission') return 'Permission mode';
|
|
1087
|
-
if (type === 'model') return 'Model';
|
|
1088
|
-
if (type === 'effort') return 'Effort';
|
|
1089
|
-
return 'Options';
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function selectIdForPicker(type) {
|
|
1093
|
-
if (type === 'permission') return 'claude-permission-select';
|
|
1094
|
-
if (type === 'model') return 'claude-model-select';
|
|
1095
|
-
if (type === 'effort') return 'claude-effort-select';
|
|
1096
|
-
return '';
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function renderClaudePicker(type) {
|
|
1100
|
-
const panel = document.getElementById('claude-picker-panel');
|
|
1101
|
-
const select = document.getElementById(selectIdForPicker(type));
|
|
1102
|
-
if (!panel || !select) return;
|
|
1103
|
-
const options = Array.from(select.options);
|
|
1104
|
-
panel.innerHTML = `<div class="claude-picker-title">${escapeHtml(pickerTitle(type))}</div>${options.map(option => {
|
|
1105
|
-
const selected = option.value === select.value;
|
|
1106
|
-
const full = pickerFullValue(option);
|
|
1107
|
-
return `<button class="claude-picker-option${selected ? ' selected' : ''}" onclick="chooseClaudePickerOption('${type}', decodePathValue('${encodePathValue(option.value)}'))">
|
|
1108
|
-
<div class="claude-picker-option-main">
|
|
1109
|
-
<span>${escapeHtml(option.textContent || option.value)}</span>
|
|
1110
|
-
${selected ? '<span class="selected-label">Selected</span>' : ''}
|
|
1111
|
-
</div>
|
|
1112
|
-
${full ? `<div class="claude-picker-option-value">${escapeHtml(full)}</div>` : ''}
|
|
1113
|
-
</button>`;
|
|
1114
|
-
}).join('')}`;
|
|
1115
|
-
panel.classList.add('active');
|
|
1116
|
-
syncClaudePickerButtons();
|
|
1117
|
-
updateTerminalControlsHeight();
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
async function toggleClaudePicker(type) {
|
|
1121
|
-
if (claudePickerOpen === type) {
|
|
1122
|
-
closeClaudePicker();
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
if (type === 'model') await refreshClaudeRuntimeConfig();
|
|
1126
|
-
closeClaudeUsagePanel();
|
|
1127
|
-
claudeResumePanelOpen = false;
|
|
1128
|
-
const resumePanel = document.getElementById('claude-resume-panel');
|
|
1129
|
-
if (resumePanel) resumePanel.classList.remove('active');
|
|
1130
|
-
claudePickerOpen = type;
|
|
1131
|
-
renderClaudePicker(type);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function chooseClaudePickerOption(type, value) {
|
|
1135
|
-
const select = document.getElementById(selectIdForPicker(type));
|
|
1136
|
-
if (!select) return;
|
|
1137
|
-
select.value = value;
|
|
1138
|
-
closeClaudePicker();
|
|
1139
|
-
updateClaudeSettingsFromControls();
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function applyClaudeRuntimeConfig(config) {
|
|
1143
|
-
if (!config || !Array.isArray(config.models)) return;
|
|
1144
|
-
claudeRuntimeConfig = config;
|
|
1145
|
-
const modelEl = document.getElementById('claude-model-select');
|
|
1146
|
-
if (modelEl) {
|
|
1147
|
-
const current = modelEl.value || claudeState.model || config.defaultModel || 'default';
|
|
1148
|
-
modelEl.innerHTML = '';
|
|
1149
|
-
config.models.forEach(item => {
|
|
1150
|
-
const option = new Option(fullModelLabel(item.value, item.resolved, item.label), item.value);
|
|
1151
|
-
option.title = item.resolved ? `${item.label}: ${item.resolved}` : item.label;
|
|
1152
|
-
option.dataset.resolved = item.resolved || '';
|
|
1153
|
-
option.dataset.shortLabel = shortModelLabel(item.value, item.resolved);
|
|
1154
|
-
modelEl.add(option);
|
|
1155
|
-
});
|
|
1156
|
-
const next = Array.from(modelEl.options).some(option => option.value === current)
|
|
1157
|
-
? current
|
|
1158
|
-
: (config.defaultModel || 'default');
|
|
1159
|
-
modelEl.value = next;
|
|
1160
|
-
claudeState.model = next;
|
|
1161
|
-
}
|
|
1162
|
-
if (config.defaultEffort && claudeState.effort === 'medium') {
|
|
1163
|
-
claudeState.effort = config.defaultEffort;
|
|
1164
|
-
}
|
|
1165
|
-
syncClaudePickerButtons();
|
|
1166
|
-
renderClaudeStateBar();
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
async function refreshClaudeRuntimeConfig() {
|
|
1170
|
-
try {
|
|
1171
|
-
const res = await fetchWithTimeout('/api/claude-config', {}, 10000);
|
|
1172
|
-
const data = await res.json();
|
|
1173
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Claude config');
|
|
1174
|
-
applyClaudeRuntimeConfig(data.config);
|
|
1175
|
-
return data.config;
|
|
1176
|
-
} catch (e) {
|
|
1177
|
-
log('Failed to load Claude config: ' + e.message);
|
|
1178
|
-
return null;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function ensureSelectOption(selectEl, value, labelPrefix) {
|
|
1183
|
-
const normalized = String(value || '').trim();
|
|
1184
|
-
if (!normalized) return;
|
|
1185
|
-
const exists = Array.from(selectEl.options).some(option => option.value === normalized);
|
|
1186
|
-
if (!exists) {
|
|
1187
|
-
const label = `${labelPrefix}:${normalized.replace(/^claude-/, '').replace(/-20\d{6,}.*$/, '').slice(0, 10)}`;
|
|
1188
|
-
selectEl.add(new Option(label, normalized));
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function applyClaudeState(state = {}) {
|
|
1193
|
-
claudeState = { ...claudeState, ...state };
|
|
1194
|
-
claudeStatus = claudeState.status || claudeStatus;
|
|
1195
|
-
const permissionEl = document.getElementById('claude-permission-select');
|
|
1196
|
-
const modelEl = document.getElementById('claude-model-select');
|
|
1197
|
-
const effortEl = document.getElementById('claude-effort-select');
|
|
1198
|
-
if (permissionEl) permissionEl.value = claudeState.permissionMode || 'default';
|
|
1199
|
-
if (modelEl) {
|
|
1200
|
-
ensureSelectOption(modelEl, claudeState.model || 'default', 'M');
|
|
1201
|
-
modelEl.value = claudeState.model || 'default';
|
|
1202
|
-
}
|
|
1203
|
-
if (effortEl) effortEl.value = claudeState.effort || 'medium';
|
|
1204
|
-
syncClaudePickerButtons();
|
|
1205
|
-
const abortBtn = document.getElementById('claude-abort-btn');
|
|
1206
|
-
if (abortBtn) abortBtn.disabled = !(claudeState.canAbort || claudeStatus === 'thinking');
|
|
1207
|
-
renderClaudeStateBar();
|
|
1208
|
-
renderClaudeUsagePanel();
|
|
1209
|
-
renderClaudeChat();
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
function shortValue(value, fallback = 'N/A') {
|
|
1213
|
-
const text = String(value || fallback);
|
|
1214
|
-
return text.length > 16 ? text.slice(0, 13) + '...' : text;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function formatTokenCount(value) {
|
|
1218
|
-
const number = Number(value || 0);
|
|
1219
|
-
if (number >= 1000000) return `${(number / 1000000).toFixed(2)}M`;
|
|
1220
|
-
if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
|
|
1221
|
-
return String(Math.round(number));
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function formatContextK(value) {
|
|
1225
|
-
return `${(Number(value || 0) / 1000).toFixed(1)}K`;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
function contextRemainingPercent(usage) {
|
|
1229
|
-
if (!usage || typeof usage.contextSize !== 'number') return null;
|
|
1230
|
-
const used = Math.max(0, usage.contextSize);
|
|
1231
|
-
return Math.max(0, Math.min(100, 100 - (used / CLAUDE_CONTEXT_SIZE) * 100));
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
function renderClaudeContextSizeBadge() {
|
|
1235
|
-
const usage = claudeState.latestUsage;
|
|
1236
|
-
if (!usage || typeof usage.contextSize !== 'number') return '';
|
|
1237
|
-
return `<div class="claude-context-size-badge">Context ${escapeHtml(formatContextK(usage.contextSize))}</div>`;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function renderClaudeStateBar() {
|
|
1241
|
-
const el = document.getElementById('claude-state-bar');
|
|
1242
|
-
if (!el) return;
|
|
1243
|
-
const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
1244
|
-
const mode = claudeState.permissionMode || 'default';
|
|
1245
|
-
const parts = [];
|
|
1246
|
-
if (pending) {
|
|
1247
|
-
parts.push(`<span class="claude-state-pill warn">${pending} approval${pending > 1 ? 's' : ''}</span>`);
|
|
1248
|
-
} else if (claudeStatus === 'thinking') {
|
|
1249
|
-
parts.push('<span class="claude-state-pill">Working</span>');
|
|
1250
|
-
} else if (claudeStatus && !['idle', 'stopped'].includes(claudeStatus)) {
|
|
1251
|
-
parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
|
|
1252
|
-
}
|
|
1253
|
-
const remainingPercent = contextRemainingPercent(claudeState.latestUsage);
|
|
1254
|
-
if (remainingPercent !== null && remainingPercent <= 10) {
|
|
1255
|
-
parts.push(`<span class="claude-state-pill warn">${Math.round(remainingPercent)}% left</span>`);
|
|
1256
|
-
}
|
|
1257
|
-
if (mode !== 'default') {
|
|
1258
|
-
parts.push(`<span class="claude-state-pill perm ${escapeHtml(mode)}">${escapeHtml(permissionModeLabel(mode))}</span>`);
|
|
1259
|
-
}
|
|
1260
|
-
el.innerHTML = parts.join('');
|
|
1261
|
-
el.style.display = parts.length ? 'flex' : 'none';
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function usageItem(label, value) {
|
|
1265
|
-
return `<div class="claude-usage-item">
|
|
1266
|
-
<div class="claude-usage-label">${escapeHtml(label)}</div>
|
|
1267
|
-
<div class="claude-usage-value">${escapeHtml(value)}</div>
|
|
1268
|
-
</div>`;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function renderClaudeUsagePanel() {
|
|
1272
|
-
const panel = document.getElementById('claude-usage-panel');
|
|
1273
|
-
if (!panel) return;
|
|
1274
|
-
const usage = claudeState.latestUsage;
|
|
1275
|
-
if (!usage) {
|
|
1276
|
-
panel.innerHTML = '<div class="claude-usage-title">Usage</div><div class="claude-resume-meta">No usage yet.</div>';
|
|
1277
|
-
} else {
|
|
1278
|
-
panel.innerHTML = `<div class="claude-usage-title">Usage</div>
|
|
1279
|
-
<div class="claude-usage-grid">
|
|
1280
|
-
${usageItem('Total', formatTokenCount(usage.totalTokens))}
|
|
1281
|
-
${usageItem('Context', formatContextK(usage.contextSize))}
|
|
1282
|
-
${usageItem('Input', formatTokenCount(usage.inputTokens))}
|
|
1283
|
-
${usageItem('Output', formatTokenCount(usage.outputTokens))}
|
|
1284
|
-
${usageItem('Cache write', formatTokenCount(usage.cacheCreation))}
|
|
1285
|
-
${usageItem('Cache read', formatTokenCount(usage.cacheRead))}
|
|
1286
|
-
</div>`;
|
|
1287
|
-
}
|
|
1288
|
-
panel.classList.toggle('active', claudeUsagePanelOpen);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function toggleClaudeUsagePanel() {
|
|
1292
|
-
claudeUsagePanelOpen = !claudeUsagePanelOpen;
|
|
1293
|
-
if (claudeUsagePanelOpen) {
|
|
1294
|
-
closeClaudePicker();
|
|
1295
|
-
claudeResumePanelOpen = false;
|
|
1296
|
-
const resumePanel = document.getElementById('claude-resume-panel');
|
|
1297
|
-
if (resumePanel) resumePanel.classList.remove('active');
|
|
1298
|
-
}
|
|
1299
|
-
renderClaudeUsagePanel();
|
|
1300
|
-
updateTerminalControlsHeight();
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
async function updateClaudeSettingsFromControls() {
|
|
1304
|
-
await refreshClaudeRuntimeConfig();
|
|
1305
|
-
const settings = {
|
|
1306
|
-
permissionMode: document.getElementById('claude-permission-select').value,
|
|
1307
|
-
model: document.getElementById('claude-model-select').value,
|
|
1308
|
-
effort: document.getElementById('claude-effort-select').value
|
|
1309
|
-
};
|
|
1310
|
-
applyClaudeState(settings);
|
|
1311
|
-
if (currentSocket && currentSocket.readyState === 1) {
|
|
1312
|
-
currentSocket.send(JSON.stringify({ type: 'claude-settings', settings }));
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
function abortClaudeSession() {
|
|
1317
|
-
if (!currentSocket || currentSocket.readyState !== 1) return;
|
|
1318
|
-
currentSocket.send(JSON.stringify({ type: 'claude-abort' }));
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
async function toggleClaudeResumePanel() {
|
|
1322
|
-
claudeResumePanelOpen = !claudeResumePanelOpen;
|
|
1323
|
-
if (claudeResumePanelOpen) closeClaudePicker();
|
|
1324
|
-
if (claudeResumePanelOpen) closeClaudeUsagePanel();
|
|
1325
|
-
const panel = document.getElementById('claude-resume-panel');
|
|
1326
|
-
panel.classList.toggle('active', claudeResumePanelOpen);
|
|
1327
|
-
updateTerminalControlsHeight();
|
|
1328
|
-
if (claudeResumePanelOpen && !claudeResumeItemsLoaded) {
|
|
1329
|
-
await loadClaudeResumeSessions();
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
async function loadClaudeResumeSessions() {
|
|
1334
|
-
const panel = document.getElementById('claude-resume-panel');
|
|
1335
|
-
if (!activeSessionId) return;
|
|
1336
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
|
|
1337
|
-
try {
|
|
1338
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/claude-resume-sessions`, {}, 15000);
|
|
1339
|
-
const data = await res.json();
|
|
1340
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load resume sessions');
|
|
1341
|
-
const items = data.items || [];
|
|
1342
|
-
claudeResumeItemsLoaded = true;
|
|
1343
|
-
if (!items.length) {
|
|
1344
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">No local Claude sessions found for this folder.</div>';
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
panel.innerHTML = items.map(item => {
|
|
1348
|
-
const encodedId = encodePathValue(item.id);
|
|
1349
|
-
const updated = item.updatedAt ? new Date(item.updatedAt).toLocaleString() : 'Unknown';
|
|
1350
|
-
const active = item.id === claudeState.resumeSessionId || item.id === claudeState.claudeSessionId;
|
|
1351
|
-
return `<button class="claude-resume-item" onclick="selectClaudeResumeSession(decodePathValue('${encodedId}'))">
|
|
1352
|
-
<div class="claude-resume-title"><span>${escapeHtml(item.id.slice(0, 8))}${active ? ' · current' : ''}</span><span>${escapeHtml(updated)}</span></div>
|
|
1353
|
-
<div class="claude-resume-meta">${escapeHtml(item.lastText || item.firstText || item.cwd || 'No preview')}</div>
|
|
1354
|
-
</button>`;
|
|
1355
|
-
}).join('');
|
|
1356
|
-
} catch (e) {
|
|
1357
|
-
panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px; color:#ff6b61;">${escapeHtml(e.message)}</div>`;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function selectClaudeResumeSession(id) {
|
|
1362
|
-
if (!id || !currentSocket || currentSocket.readyState !== 1) return;
|
|
1363
|
-
currentSocket.send(JSON.stringify({ type: 'claude-resume', resumeSessionId: id }));
|
|
1364
|
-
applyClaudeState({ resumeSessionId: id, claudeSessionId: id });
|
|
1365
|
-
claudeResumePanelOpen = false;
|
|
1366
|
-
document.getElementById('claude-resume-panel').classList.remove('active');
|
|
1367
|
-
updateTerminalControlsHeight();
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function textFromClaudeMessage(message) {
|
|
1371
|
-
if (!message) return '';
|
|
1372
|
-
if (message.text) return String(message.text);
|
|
1373
|
-
if (message.summary) return String(message.summary);
|
|
1374
|
-
return '';
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function escapeRegExp(text) {
|
|
1378
|
-
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
function inlineMarkdown(text) {
|
|
1382
|
-
let html = escapeHtml(text || '');
|
|
1383
|
-
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1384
|
-
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1385
|
-
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
1386
|
-
html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
|
|
1387
|
-
html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
|
|
1388
|
-
html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, '<img src="$2" alt="$1">');
|
|
1389
|
-
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
1390
|
-
return html;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
function splitMarkdownBlocks(markdown) {
|
|
1394
|
-
const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
|
|
1395
|
-
const blocks = [];
|
|
1396
|
-
let i = 0;
|
|
1397
|
-
while (i < lines.length) {
|
|
1398
|
-
if (!lines[i].trim()) {
|
|
1399
|
-
i++;
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
const fence = lines[i].match(/^```(\w+)?\s*$/);
|
|
1403
|
-
if (fence) {
|
|
1404
|
-
const language = fence[1] || '';
|
|
1405
|
-
const content = [];
|
|
1406
|
-
i++;
|
|
1407
|
-
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
|
1408
|
-
content.push(lines[i]);
|
|
1409
|
-
i++;
|
|
1410
|
-
}
|
|
1411
|
-
if (i < lines.length) i++;
|
|
1412
|
-
blocks.push({ type: 'code', language, content: content.join('\n') });
|
|
1413
|
-
continue;
|
|
1414
|
-
}
|
|
1415
|
-
if (/^\s*[-*_]{3,}\s*$/.test(lines[i])) {
|
|
1416
|
-
blocks.push({ type: 'hr' });
|
|
1417
|
-
i++;
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
if (/^\s{0,3}#{1,4}\s+/.test(lines[i])) {
|
|
1421
|
-
const match = lines[i].match(/^(\s{0,3})(#{1,4})\s+(.+)$/);
|
|
1422
|
-
blocks.push({ type: 'header', level: match[2].length, text: match[3] });
|
|
1423
|
-
i++;
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
if (lines[i].includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1])) {
|
|
1427
|
-
const rows = [lines[i]];
|
|
1428
|
-
i += 2;
|
|
1429
|
-
while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
|
|
1430
|
-
rows.push(lines[i]);
|
|
1431
|
-
i++;
|
|
1432
|
-
}
|
|
1433
|
-
blocks.push({ type: 'table', rows });
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
if (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) {
|
|
1437
|
-
const ordered = /^\s*\d+\.\s+/.test(lines[i]);
|
|
1438
|
-
const items = [];
|
|
1439
|
-
while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*+]\s+/.test(lines[i]))) {
|
|
1440
|
-
items.push(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*+]\s+/, ''));
|
|
1441
|
-
i++;
|
|
1442
|
-
}
|
|
1443
|
-
blocks.push({ type: ordered ? 'ol' : 'ul', items });
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
if (/^\s*>\s?/.test(lines[i])) {
|
|
1447
|
-
const quote = [];
|
|
1448
|
-
while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
|
|
1449
|
-
quote.push(lines[i].replace(/^\s*>\s?/, ''));
|
|
1450
|
-
i++;
|
|
1451
|
-
}
|
|
1452
|
-
blocks.push({ type: 'quote', text: quote.join('\n') });
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
const paragraph = [];
|
|
1456
|
-
while (i < lines.length && lines[i].trim()) {
|
|
1457
|
-
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;
|
|
1458
|
-
paragraph.push(lines[i]);
|
|
1459
|
-
i++;
|
|
1460
|
-
}
|
|
1461
|
-
blocks.push({ type: 'paragraph', text: paragraph.join('\n') });
|
|
1462
|
-
}
|
|
1463
|
-
return blocks;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function renderMarkdown(markdown) {
|
|
1467
|
-
const blocks = splitMarkdownBlocks(markdown);
|
|
1468
|
-
return `<div class="claude-md">${blocks.map(block => {
|
|
1469
|
-
if (block.type === 'code') {
|
|
1470
|
-
const language = block.language ? `<div class="claude-tool-section-title">${escapeHtml(block.language)}</div>` : '';
|
|
1471
|
-
return `<pre>${language}<code>${escapeHtml(block.content)}</code></pre>`;
|
|
1472
|
-
}
|
|
1473
|
-
if (block.type === 'hr') return '<hr>';
|
|
1474
|
-
if (block.type === 'header') return `<h${block.level}>${inlineMarkdown(block.text)}</h${block.level}>`;
|
|
1475
|
-
if (block.type === 'ul') return `<ul>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ul>`;
|
|
1476
|
-
if (block.type === 'ol') return `<ol>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
|
|
1477
|
-
if (block.type === 'quote') return `<blockquote>${renderMarkdown(block.text)}</blockquote>`;
|
|
1478
|
-
if (block.type === 'table') return renderMarkdownTable(block.rows);
|
|
1479
|
-
return `<p>${inlineMarkdown(block.text).replace(/\n/g, '<br>')}</p>`;
|
|
1480
|
-
}).join('')}</div>`;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
function renderMarkdownTable(rows) {
|
|
1484
|
-
const parsed = rows.map(row => row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim()));
|
|
1485
|
-
if (!parsed.length) return '';
|
|
1486
|
-
const [head, ...body] = parsed;
|
|
1487
|
-
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>`;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
function parseLocalCommandMessage(text) {
|
|
1491
|
-
const value = String(text || '');
|
|
1492
|
-
if (/^\s*<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*$/.test(value)) return { kind: 'hidden' };
|
|
1493
|
-
const stdoutMatch = value.match(/^\s*<local-command-stdout>\s*([\s\S]*?)\s*<\/local-command-stdout>\s*$/);
|
|
1494
|
-
if (stdoutMatch) {
|
|
1495
|
-
const stdout = stdoutMatch[1].trim();
|
|
1496
|
-
if (/^Goal set:/i.test(stdout)) return { kind: 'hidden' };
|
|
1497
|
-
return { kind: 'text', text: stdout };
|
|
1498
|
-
}
|
|
1499
|
-
const raw = value.trim().match(/^\/([a-zA-Z][\w:-]*)(?:\s+([\s\S]*?))?$/);
|
|
1500
|
-
if (raw) {
|
|
1501
|
-
return { kind: 'command', commandName: raw[1], args: raw[2] && raw[2].trim() };
|
|
1502
|
-
}
|
|
1503
|
-
const nameMatch = value.match(/<command-name>\s*\/?([^<]+?)\s*<\/command-name>/);
|
|
1504
|
-
if (nameMatch) {
|
|
1505
|
-
const argsMatch = value.match(/<command-args>\s*([\s\S]*?)\s*<\/command-args>/);
|
|
1506
|
-
const stripped = value
|
|
1507
|
-
.replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
|
|
1508
|
-
.replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
|
|
1509
|
-
.replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
|
|
1510
|
-
.trim();
|
|
1511
|
-
if (!stripped) return { kind: 'command', commandName: nameMatch[1].trim(), args: argsMatch && argsMatch[1].trim() };
|
|
1512
|
-
return { kind: 'text', text: stripped };
|
|
1513
|
-
}
|
|
1514
|
-
return { kind: 'text', text: value };
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function toolCategory(name) {
|
|
1518
|
-
if (['Bash', 'CodexBash', 'execute'].includes(name)) return 'terminal';
|
|
1519
|
-
if (['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name)) return 'edit';
|
|
1520
|
-
if (['Read', 'LS'].includes(name)) return 'read';
|
|
1521
|
-
if (['Grep', 'Glob'].includes(name)) return 'search';
|
|
1522
|
-
if (['WebFetch', 'WebSearch'].includes(name)) return 'web';
|
|
1523
|
-
if (['Task', 'Agent'].includes(name)) return 'task';
|
|
1524
|
-
return 'other';
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function toolIcon(category) {
|
|
1528
|
-
const labels = { terminal: '$', edit: '+/-', read: 'R', search: '?', web: 'W', task: 'A', other: '*' };
|
|
1529
|
-
return labels[category] || '*';
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
function toolCommand(tool) {
|
|
1533
|
-
const input = tool && tool.input;
|
|
1534
|
-
if (!input || typeof input !== 'object') return '';
|
|
1535
|
-
if (typeof input.command === 'string') return input.command;
|
|
1536
|
-
if (Array.isArray(input.command)) return input.command.join(' ');
|
|
1537
|
-
if (typeof input.file_path === 'string') return input.file_path;
|
|
1538
|
-
if (typeof input.path === 'string') return input.path;
|
|
1539
|
-
if (typeof input.pattern === 'string') return input.pattern;
|
|
1540
|
-
if (typeof input.prompt === 'string') return input.prompt;
|
|
1541
|
-
return tool.summary || '';
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
function toolTitle(tool) {
|
|
1545
|
-
const name = tool.name || 'Tool';
|
|
1546
|
-
const input = tool.input || {};
|
|
1547
|
-
if (name === 'Bash') return (tool.description || 'Terminal');
|
|
1548
|
-
if (name === 'Read' && input.file_path) return input.file_path;
|
|
1549
|
-
if (name === 'Edit' || name === 'MultiEdit' || name === 'Write') return input.file_path || name;
|
|
1550
|
-
if (name === 'Grep') return input.pattern ? `grep(pattern: ${input.pattern})` : 'Search Content';
|
|
1551
|
-
if (name === 'Glob') return input.pattern || 'Search Files';
|
|
1552
|
-
if ((name === 'Task' || name === 'Agent') && input.description) return input.description;
|
|
1553
|
-
if (name.startsWith('mcp__')) return name.replace(/^mcp__/, '').replace(/__/g, ': ');
|
|
1554
|
-
return name;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function renderToolSection(title, value) {
|
|
1558
|
-
if (value === undefined || value === null || value === '') return '';
|
|
1559
|
-
const code = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
1560
|
-
return `<div class="claude-tool-section"><div class="claude-tool-section-title">${escapeHtml(title)}</div><pre class="claude-tool-code">${escapeHtml(code)}</pre></div>`;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
function renderClaudeTool(tool) {
|
|
1564
|
-
const category = toolCategory(tool.name || '');
|
|
1565
|
-
const command = toolCommand(tool);
|
|
1566
|
-
const hasResult = tool.resultText || tool.isError;
|
|
1567
|
-
const running = claudeStatus === 'thinking' && !hasResult;
|
|
1568
|
-
const status = tool.isError ? 'error' : (hasResult ? 'completed' : (running ? 'running' : 'running'));
|
|
1569
|
-
const open = tool.isError || (!hasResult && !running) ? ' open' : '';
|
|
1570
|
-
const compact = category === 'terminal' || category === 'search' || category === 'read';
|
|
1571
|
-
return `<details class="claude-tool claude-tool-card${tool.isError ? ' error' : ''}"${open}>
|
|
1572
|
-
<summary class="claude-tool-header">
|
|
1573
|
-
<span class="claude-tool-icon" data-icon="${escapeHtml(toolIcon(category))}"></span>
|
|
1574
|
-
<span class="claude-tool-title">${escapeHtml(toolTitle(tool))}</span>
|
|
1575
|
-
<span class="claude-tool-status">${escapeHtml(status)}</span>
|
|
1576
|
-
<span class="claude-tool-command">${escapeHtml(command)}</span>
|
|
1577
|
-
</summary>
|
|
1578
|
-
${compact && !tool.isError ? '' : `<div class="claude-tool-body">
|
|
1579
|
-
${renderToolSection('Input', tool.input)}
|
|
1580
|
-
${renderToolSection(tool.isError ? 'Error' : 'Output', tool.resultText)}
|
|
1581
|
-
</div>`}
|
|
1582
|
-
</details>`;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function renderWorkGroup(items) {
|
|
1586
|
-
const running = items.some(item => item.kind === 'tool' && !item.resultText);
|
|
1587
|
-
const label = `${items.length} tool call${items.length > 1 ? 's' : ''}`;
|
|
1588
|
-
return `<details class="claude-work-group"${running ? ' open' : ''}>
|
|
1589
|
-
<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>
|
|
1590
|
-
<div class="claude-work-group-body">${items.map(renderDisplayItem).join('')}</div>
|
|
1591
|
-
</details>`;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
function buildClaudeDisplayItems(messages) {
|
|
1595
|
-
const items = [];
|
|
1596
|
-
const byToolUseId = new Map();
|
|
1597
|
-
for (const message of messages) {
|
|
1598
|
-
if (!message) continue;
|
|
1599
|
-
if (message.kind === 'tool') {
|
|
1600
|
-
const item = { ...message, kind: 'tool', resultText: '', isError: false };
|
|
1601
|
-
items.push(item);
|
|
1602
|
-
if (item.toolUseId) byToolUseId.set(item.toolUseId, item);
|
|
1603
|
-
continue;
|
|
1604
|
-
}
|
|
1605
|
-
if (message.kind === 'tool-result') {
|
|
1606
|
-
const parent = message.toolUseId && byToolUseId.get(message.toolUseId);
|
|
1607
|
-
if (parent) {
|
|
1608
|
-
parent.resultText = message.text || '';
|
|
1609
|
-
parent.isError = Boolean(message.isError);
|
|
1610
|
-
} else {
|
|
1611
|
-
items.push(message);
|
|
1612
|
-
}
|
|
1613
|
-
continue;
|
|
1614
|
-
}
|
|
1615
|
-
items.push(message);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
const grouped = [];
|
|
1619
|
-
let run = [];
|
|
1620
|
-
const flush = () => {
|
|
1621
|
-
if (run.length > 1) grouped.push({ kind: 'work-group', items: run });
|
|
1622
|
-
else if (run.length === 1) grouped.push(run[0]);
|
|
1623
|
-
run = [];
|
|
1624
|
-
};
|
|
1625
|
-
for (const item of items) {
|
|
1626
|
-
if (item.kind === 'tool') {
|
|
1627
|
-
run.push(item);
|
|
1628
|
-
continue;
|
|
1629
|
-
}
|
|
1630
|
-
flush();
|
|
1631
|
-
grouped.push(item);
|
|
1632
|
-
}
|
|
1633
|
-
flush();
|
|
1634
|
-
return grouped;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
function renderUserMessage(message) {
|
|
1638
|
-
const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
|
|
1639
|
-
if (parsed.kind === 'hidden') return '';
|
|
1640
|
-
if (parsed.kind === 'command') {
|
|
1641
|
-
const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
|
|
1642
|
-
return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span></div>`;
|
|
1643
|
-
}
|
|
1644
|
-
return `<div class="claude-message user">${renderMarkdown(parsed.text)}</div>`;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
function renderDisplayItem(message) {
|
|
1648
|
-
if (!message) return '';
|
|
1649
|
-
if (message.kind === 'user') return renderUserMessage(message);
|
|
1650
|
-
if (message.kind === 'assistant') return `<div class="claude-message assistant">${renderMarkdown(textFromClaudeMessage(message))}</div>`;
|
|
1651
|
-
if (message.kind === 'tool') return renderClaudeTool(message);
|
|
1652
|
-
if (message.kind === 'tool-result') return `<div class="claude-tool${message.isError ? ' error' : ''}">${renderToolSection(message.isError ? 'Error' : 'Output', message.text || '')}</div>`;
|
|
1653
|
-
if (message.kind === 'work-group') return renderWorkGroup(message.items);
|
|
1654
|
-
if (message.kind === 'event') return `<div class="claude-message event${message.level === 'error' ? ' error' : ''}">${escapeHtml(message.text || '')}</div>`;
|
|
1655
|
-
return '';
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
function isClaudeEditTool(name) {
|
|
1659
|
-
return ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name || '');
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function isClaudeExitPlanTool(name) {
|
|
1663
|
-
return name === 'exit_plan_mode' || name === 'ExitPlanMode';
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
function claudeAllowToolLabel(req) {
|
|
1667
|
-
if (req.toolName === 'Bash' && req.input && typeof req.input.command === 'string') return 'Allow command';
|
|
1668
|
-
return 'Allow tool';
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
function renderClaudePermissionActions(req) {
|
|
1672
|
-
const id = escapeHtml(req.id);
|
|
1673
|
-
const toolName = req.toolName || '';
|
|
1674
|
-
const parts = [
|
|
1675
|
-
`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-once')">Yes</button>`
|
|
1676
|
-
];
|
|
1677
|
-
if (isClaudeEditTool(toolName) || isClaudeExitPlanTool(toolName) || req.canAllowEdits) {
|
|
1678
|
-
parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-edits')">Allow edits</button>`);
|
|
1679
|
-
}
|
|
1680
|
-
if (isClaudeExitPlanTool(toolName) || req.canBypass) {
|
|
1681
|
-
parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'bypass')">Allow all</button>`);
|
|
1682
|
-
}
|
|
1683
|
-
if (toolName && !isClaudeEditTool(toolName) && !isClaudeExitPlanTool(toolName) && req.canAllowTool !== false) {
|
|
1684
|
-
parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-tool')">${escapeHtml(claudeAllowToolLabel(req))}</button>`);
|
|
1685
|
-
}
|
|
1686
|
-
parts.push(`<button class="small-btn danger" onclick="respondClaudePermission('${id}', 'deny')">Deny</button>`);
|
|
1687
|
-
return parts.join('');
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
function renderClaudeChat() {
|
|
1691
|
-
const container = document.getElementById('claude-chat-container');
|
|
1692
|
-
const parts = [renderClaudeContextSizeBadge(), ...buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem)].filter(Boolean);
|
|
1693
|
-
for (const req of claudePendingPermissions.filter(item => item.status === 'pending')) {
|
|
1694
|
-
parts.push(`<div class="claude-tool claude-permission">
|
|
1695
|
-
<div class="claude-tool-header">
|
|
1696
|
-
<span class="claude-tool-icon" data-icon="!"></span>
|
|
1697
|
-
<strong>${escapeHtml(req.title || req.toolName || 'Permission required')}</strong>
|
|
1698
|
-
</div>
|
|
1699
|
-
<div class="claude-tool-body">
|
|
1700
|
-
${req.reason ? `<div>${escapeHtml(req.reason)}</div>` : ''}
|
|
1701
|
-
${req.blockedPath ? `<div class="claude-resume-meta">${escapeHtml(req.blockedPath)}</div>` : ''}
|
|
1702
|
-
${renderToolSection('Input', req.input || {})}
|
|
1703
|
-
<div class="claude-permission-actions">
|
|
1704
|
-
${renderClaudePermissionActions(req)}
|
|
1705
|
-
</div>
|
|
1706
|
-
</div>
|
|
1707
|
-
</div>`);
|
|
1708
|
-
}
|
|
1709
|
-
if (claudeStatus === 'thinking') {
|
|
1710
|
-
parts.push('<div class="claude-status">Claude is working...</div>');
|
|
1711
|
-
}
|
|
1712
|
-
container.innerHTML = parts.join('') || '<div class="claude-message event">Send a message to start Claude.</div>';
|
|
1713
|
-
renderClaudeStateBar();
|
|
1714
|
-
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
function applyClaudeEvent(event) {
|
|
1718
|
-
if (!event) return;
|
|
1719
|
-
if (event.type === 'message' && event.message) {
|
|
1720
|
-
claudeMessages.push(event.message);
|
|
1721
|
-
} else if (event.type === 'status') {
|
|
1722
|
-
claudeStatus = event.status || 'idle';
|
|
1723
|
-
} else if (event.type === 'permission-request' && event.request) {
|
|
1724
|
-
claudePendingPermissions = claudePendingPermissions.filter(item => item.id !== event.request.id);
|
|
1725
|
-
claudePendingPermissions.push(event.request);
|
|
1726
|
-
} else if (event.type === 'permission-updated' && event.request) {
|
|
1727
|
-
claudePendingPermissions = claudePendingPermissions.map(item => item.id === event.request.id ? event.request : item);
|
|
1728
|
-
} else if (event.type === 'history-reset' && Array.isArray(event.messages)) {
|
|
1729
|
-
claudeMessages = event.messages;
|
|
1730
|
-
} else if (event.type === 'state' && event.state) {
|
|
1731
|
-
applyClaudeState(event.state);
|
|
1732
|
-
}
|
|
1733
|
-
if (event.type !== 'state') applyClaudeState({
|
|
1734
|
-
status: claudeStatus,
|
|
1735
|
-
pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
|
|
1736
|
-
canAbort: claudeStatus === 'thinking'
|
|
1737
|
-
});
|
|
1738
|
-
renderClaudeChat();
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
function respondClaudePermission(id, actionOrApproved) {
|
|
1742
|
-
if (!currentSocket || currentSocket.readyState !== 1) return;
|
|
1743
|
-
const action = typeof actionOrApproved === 'string'
|
|
1744
|
-
? actionOrApproved
|
|
1745
|
-
: (actionOrApproved ? 'allow-once' : 'deny');
|
|
1746
|
-
currentSocket.send(JSON.stringify({
|
|
1747
|
-
type: 'claude-permission',
|
|
1748
|
-
id,
|
|
1749
|
-
action,
|
|
1750
|
-
approved: action !== 'deny'
|
|
1751
|
-
}));
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
async function createSession(toolKey, sessionName) {
|
|
1755
|
-
try {
|
|
1756
|
-
const workingDirectory = document.getElementById('cwd-field').value;
|
|
1757
|
-
const runtimeConfig = toolKey === 'claude-code' ? await refreshClaudeRuntimeConfig() : null;
|
|
1758
|
-
const claudeOptions = runtimeConfig ? {
|
|
1759
|
-
model: runtimeConfig.defaultModel || 'default',
|
|
1760
|
-
effort: runtimeConfig.defaultEffort || 'medium'
|
|
1761
|
-
} : undefined;
|
|
1762
|
-
const res = await fetchWithTimeout('/api/sessions', {
|
|
1763
|
-
method: 'POST',
|
|
1764
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1765
|
-
body: JSON.stringify({ toolKey, workingDirectory, claudeOptions })
|
|
1766
|
-
});
|
|
1767
|
-
const data = await res.json();
|
|
1768
|
-
if (!res.ok || !data.id) throw new Error(data.error || 'Failed to create session');
|
|
1769
|
-
document.getElementById('modal-overlay').style.display = 'none';
|
|
1770
|
-
refreshSessionsNow();
|
|
1771
|
-
} catch (e) { alert('Failed to create session: ' + e.message); }
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
async function ensureScheduleTools() {
|
|
1775
|
-
if (scheduleTools.length) return;
|
|
1776
|
-
const res = await fetchWithTimeout('/api/tools');
|
|
1777
|
-
scheduleTools = await res.json();
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
function defaultSchedule() {
|
|
1781
|
-
return {
|
|
1782
|
-
id: null,
|
|
1783
|
-
name: 'Scheduled Task',
|
|
1784
|
-
enabled: true,
|
|
1785
|
-
schedule: { time: '09:00', weekdays: [1, 2, 3, 4, 5] },
|
|
1786
|
-
target: { toolKey: (scheduleTools[0] && scheduleTools[0].key) || 'demo', workingDirectory: '' },
|
|
1787
|
-
steps: [
|
|
1788
|
-
{ type: 'sleep', seconds: 60 },
|
|
1789
|
-
{ type: 'sendText', text: 'hello' },
|
|
1790
|
-
{ type: 'sleep', seconds: 1 },
|
|
1791
|
-
{ type: 'sendKey', key: 'enter' },
|
|
1792
|
-
{ type: 'sleep', seconds: 60 },
|
|
1793
|
-
{ type: 'closeSession' }
|
|
1794
|
-
]
|
|
1795
|
-
};
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
async function showScheduleModal(job = null) {
|
|
1799
|
-
try {
|
|
1800
|
-
await ensureScheduleTools();
|
|
1801
|
-
loadAppConfig();
|
|
1802
|
-
} catch (e) {
|
|
1803
|
-
alert('Failed to load tools');
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
const data = job || defaultSchedule();
|
|
1808
|
-
editingScheduleId = data.id || null;
|
|
1809
|
-
editingSteps = (data.steps || []).map(step => ({ ...step }));
|
|
1810
|
-
selectedWeekdays = [...(data.schedule.weekdays || [1, 2, 3, 4, 5])];
|
|
1811
|
-
document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Schedule' : 'New Schedule';
|
|
1812
|
-
document.getElementById('schedule-name').value = data.name || 'Scheduled Task';
|
|
1813
|
-
document.getElementById('schedule-cwd').value = data.target.workingDirectory || '';
|
|
1814
|
-
document.getElementById('schedule-time').value = data.schedule.time || '09:00';
|
|
1815
|
-
|
|
1816
|
-
const toolSelect = document.getElementById('schedule-tool');
|
|
1817
|
-
toolSelect.innerHTML = scheduleTools.map(t => `<option value="${escapeHtml(t.key)}">${escapeHtml(t.displayName)}</option>`).join('');
|
|
1818
|
-
const requestedTool = data.target.toolKey || '';
|
|
1819
|
-
toolSelect.value = scheduleTools.some(t => t.key === requestedTool) ? requestedTool : ((scheduleTools[0] && scheduleTools[0].key) || 'demo');
|
|
1820
|
-
renderWeekdays();
|
|
1821
|
-
renderScheduleSteps();
|
|
1822
|
-
document.getElementById('schedule-modal-overlay').style.display = 'flex';
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function closeScheduleModal(e) {
|
|
1826
|
-
if (!e || e.target.id === 'schedule-modal-overlay') {
|
|
1827
|
-
document.getElementById('schedule-modal-overlay').style.display = 'none';
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function renderWeekdays() {
|
|
1832
|
-
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1833
|
-
document.getElementById('schedule-weekdays').innerHTML = labels.map((label, day) => {
|
|
1834
|
-
const active = selectedWeekdays.includes(day);
|
|
1835
|
-
return `<button type="button" class="weekday-btn ${active ? 'active' : ''}" onclick="toggleWeekday(${day})">${label}</button>`;
|
|
1836
|
-
}).join('');
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
function toggleWeekday(day) {
|
|
1840
|
-
if (selectedWeekdays.includes(day)) {
|
|
1841
|
-
selectedWeekdays = selectedWeekdays.filter(value => value !== day);
|
|
1842
|
-
} else {
|
|
1843
|
-
selectedWeekdays.push(day);
|
|
1844
|
-
selectedWeekdays.sort((a, b) => a - b);
|
|
1845
|
-
}
|
|
1846
|
-
renderWeekdays();
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
function addScheduleStep(type) {
|
|
1850
|
-
if (!type) return;
|
|
1851
|
-
const defaults = {
|
|
1852
|
-
sleep: { type: 'sleep', seconds: 1 },
|
|
1853
|
-
sendText: { type: 'sendText', text: '' },
|
|
1854
|
-
sendKey: { type: 'sendKey', key: 'enter' },
|
|
1855
|
-
keyDown: { type: 'keyDown', key: 'ctrl' },
|
|
1856
|
-
keyUp: { type: 'keyUp', key: 'ctrl' },
|
|
1857
|
-
stop: { type: 'stop' },
|
|
1858
|
-
closeSession: { type: 'closeSession' }
|
|
1859
|
-
};
|
|
1860
|
-
editingSteps.push(defaults[type] || { type });
|
|
1861
|
-
renderScheduleSteps();
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
function updateStep(index, field, value) {
|
|
1865
|
-
if (!editingSteps[index]) return;
|
|
1866
|
-
if (field === 'seconds') editingSteps[index][field] = Math.max(0, Number(value) || 0);
|
|
1867
|
-
else editingSteps[index][field] = value;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
function moveStep(index, delta) {
|
|
1871
|
-
const nextIndex = index + delta;
|
|
1872
|
-
if (nextIndex < 0 || nextIndex >= editingSteps.length) return;
|
|
1873
|
-
const [step] = editingSteps.splice(index, 1);
|
|
1874
|
-
editingSteps.splice(nextIndex, 0, step);
|
|
1875
|
-
renderScheduleSteps();
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
function copyStep(index) {
|
|
1879
|
-
editingSteps.splice(index + 1, 0, { ...editingSteps[index] });
|
|
1880
|
-
renderScheduleSteps();
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
function removeStep(index) {
|
|
1884
|
-
editingSteps.splice(index, 1);
|
|
1885
|
-
renderScheduleSteps();
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
function stepInputHtml(step, index) {
|
|
1889
|
-
if (step.type === 'sleep') {
|
|
1890
|
-
return `<input type="number" min="0" step="0.1" value="${escapeHtml(step.seconds || 0)}" onchange="updateStep(${index}, 'seconds', this.value)">`;
|
|
1891
|
-
}
|
|
1892
|
-
if (step.type === 'sendText') {
|
|
1893
|
-
return `<textarea rows="2" placeholder="Text to send" onchange="updateStep(${index}, 'text', this.value)" oninput="updateStep(${index}, 'text', this.value)">${escapeHtml(step.text || '')}</textarea>`;
|
|
1894
|
-
}
|
|
1895
|
-
if (step.type === 'sendKey') {
|
|
1896
|
-
return `<select onchange="updateStep(${index}, 'key', this.value)">
|
|
1897
|
-
${['enter','tab','esc','up','down','left','right','backspace','delete','home','end','ctrl+c','ctrl+d','ctrl+l'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
|
|
1898
|
-
</select>`;
|
|
1899
|
-
}
|
|
1900
|
-
if (step.type === 'keyDown' || step.type === 'keyUp') {
|
|
1901
|
-
return `<select onchange="updateStep(${index}, 'key', this.value)">
|
|
1902
|
-
${['ctrl','alt'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
|
|
1903
|
-
</select>`;
|
|
1904
|
-
}
|
|
1905
|
-
if (step.type === 'closeSession') {
|
|
1906
|
-
return '<span style="color:var(--text-dim); font-size:13px;">Close the session and end this run</span>';
|
|
1907
|
-
}
|
|
1908
|
-
return '<span style="color:var(--text-dim); font-size:13px;">End this run</span>';
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
function renderScheduleSteps() {
|
|
1912
|
-
const container = document.getElementById('schedule-steps');
|
|
1913
|
-
if (!editingSteps.length) {
|
|
1914
|
-
container.innerHTML = '<p style="color:var(--text-dim);">No steps yet.</p>';
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
container.innerHTML = editingSteps.map((step, index) => `
|
|
1918
|
-
<div class="step-card">
|
|
1919
|
-
<div class="step-grid">
|
|
1920
|
-
<select onchange="editingSteps[${index}] = { type: this.value }; addDefaultStepFields(${index}); renderScheduleSteps();">
|
|
1921
|
-
${['sleep','sendText','sendKey','keyDown','keyUp','stop','closeSession'].map(type => `<option value="${type}" ${step.type === type ? 'selected' : ''}>${type}</option>`).join('')}
|
|
1922
|
-
</select>
|
|
1923
|
-
<div>${stepInputHtml(step, index)}</div>
|
|
1924
|
-
<div style="display:flex; gap:6px; justify-content:flex-end;">
|
|
1925
|
-
<button class="small-btn" onclick="moveStep(${index}, -1)">↑</button>
|
|
1926
|
-
<button class="small-btn" onclick="moveStep(${index}, 1)">↓</button>
|
|
1927
|
-
<button class="small-btn" onclick="copyStep(${index})">Copy</button>
|
|
1928
|
-
<button class="small-btn danger" onclick="removeStep(${index})">Del</button>
|
|
1929
|
-
</div>
|
|
1930
|
-
</div>
|
|
1931
|
-
</div>
|
|
1932
|
-
`).join('');
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
function addDefaultStepFields(index) {
|
|
1936
|
-
const type = editingSteps[index].type;
|
|
1937
|
-
if (type === 'sleep') editingSteps[index].seconds = 1;
|
|
1938
|
-
if (type === 'sendText') editingSteps[index].text = '';
|
|
1939
|
-
if (type === 'sendKey') editingSteps[index].key = 'enter';
|
|
1940
|
-
if (type === 'keyDown' || type === 'keyUp') editingSteps[index].key = 'ctrl';
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
function collectSchedulePayload() {
|
|
1944
|
-
if (!selectedWeekdays.length) throw new Error('Select at least one weekday');
|
|
1945
|
-
return {
|
|
1946
|
-
name: document.getElementById('schedule-name').value.trim() || 'Scheduled Task',
|
|
1947
|
-
enabled: true,
|
|
1948
|
-
schedule: {
|
|
1949
|
-
time: document.getElementById('schedule-time').value || '09:00',
|
|
1950
|
-
weekdays: selectedWeekdays
|
|
1951
|
-
},
|
|
1952
|
-
target: {
|
|
1953
|
-
toolKey: document.getElementById('schedule-tool').value,
|
|
1954
|
-
workingDirectory: document.getElementById('schedule-cwd').value.trim()
|
|
1955
|
-
},
|
|
1956
|
-
steps: editingSteps
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
async function saveSchedule() {
|
|
1961
|
-
try {
|
|
1962
|
-
const payload = collectSchedulePayload();
|
|
1963
|
-
const url = editingScheduleId ? `/api/schedules/${editingScheduleId}` : '/api/schedules';
|
|
1964
|
-
const method = editingScheduleId ? 'PATCH' : 'POST';
|
|
1965
|
-
const res = await fetchWithTimeout(url, {
|
|
1966
|
-
method,
|
|
1967
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1968
|
-
body: JSON.stringify(payload)
|
|
1969
|
-
});
|
|
1970
|
-
if (!res.ok) throw new Error((await res.json()).error || 'Save failed');
|
|
1971
|
-
closeScheduleModal();
|
|
1972
|
-
switchLobbyTab('schedules');
|
|
1973
|
-
} catch (e) {
|
|
1974
|
-
alert(e.message);
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
async function editSchedule(id) {
|
|
1979
|
-
const res = await fetchWithTimeout(`/api/schedules/${id}`);
|
|
1980
|
-
if (!res.ok) return alert('Failed to load schedule');
|
|
1981
|
-
showScheduleModal(await res.json());
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
async function simulateSchedule(id) {
|
|
1985
|
-
try {
|
|
1986
|
-
const res = await fetchWithTimeout(`/api/schedules/${id}/simulate`, { method: 'POST' }, 30000);
|
|
1987
|
-
const data = await res.json();
|
|
1988
|
-
if (!res.ok) throw new Error(data.error || 'Test failed');
|
|
1989
|
-
await loadSchedules();
|
|
1990
|
-
if (data.sessionId) {
|
|
1991
|
-
const jobRes = await fetchWithTimeout(`/api/schedules/${id}`).catch(() => null);
|
|
1992
|
-
const job = jobRes && jobRes.ok ? await jobRes.json() : null;
|
|
1993
|
-
joinSession(data.sessionId, 'Schedule Test', job?.target?.toolKey || null);
|
|
1994
|
-
}
|
|
1995
|
-
} catch (e) {
|
|
1996
|
-
alert(e.message);
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
async function duplicateSchedule(id) {
|
|
2001
|
-
await fetchWithTimeout(`/api/schedules/${id}/duplicate`, { method: 'POST' });
|
|
2002
|
-
loadSchedules();
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
async function toggleSchedule(id, enabled) {
|
|
2006
|
-
await fetchWithTimeout(`/api/schedules/${id}/enabled`, {
|
|
2007
|
-
method: 'PATCH',
|
|
2008
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2009
|
-
body: JSON.stringify({ enabled })
|
|
2010
|
-
});
|
|
2011
|
-
loadSchedules();
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
async function deleteSchedule(id) {
|
|
2015
|
-
if (!confirm('Delete scheduled task?')) return;
|
|
2016
|
-
await fetchWithTimeout(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
2017
|
-
loadSchedules();
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
async function deleteSession(id, e) {
|
|
2021
|
-
e.stopPropagation(); if (!confirm('Terminate session?')) return;
|
|
2022
|
-
try { await fetchWithTimeout('/api/sessions/' + id, { method: 'DELETE' }); refreshSessionsNow(); } catch (e) { alert('Failed to delete'); }
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
function showLobby() {
|
|
2026
|
-
if (currentSocket) { currentSocket.close(); currentSocket = null; }
|
|
2027
|
-
closeTimedSendPanel();
|
|
2028
|
-
stopTimedInputTimers();
|
|
2029
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
2030
|
-
document.getElementById('lobby-view').classList.add('active');
|
|
2031
|
-
refreshSessionsNow();
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
function updateToolShortcuts(toolKey) {
|
|
2035
|
-
const usesUsageCommand = ['antigravity', 'claude-code'].includes(toolKey);
|
|
2036
|
-
const usageCommand = usesUsageCommand ? '/usage' : '/stat';
|
|
2037
|
-
const defaultShortcuts = [
|
|
2038
|
-
{ key: 'left', label: '←', special: true },
|
|
2039
|
-
{ key: 'right', label: '→', special: true },
|
|
2040
|
-
{ key: 'ctrl-c', label: 'Ctrl+C' },
|
|
2041
|
-
{ command: '/model' },
|
|
2042
|
-
{ command: '/resume' },
|
|
2043
|
-
{ command: usageCommand }
|
|
2044
|
-
];
|
|
2045
|
-
const toolShortcuts = {
|
|
2046
|
-
codex: [
|
|
2047
|
-
{ key: 'ctrl-c', label: 'Ctrl+C' },
|
|
2048
|
-
{ key: 'codex-perm', label: '/perm', title: 'Send /perm' },
|
|
2049
|
-
{ command: '/model' },
|
|
2050
|
-
{ command: '/resume' },
|
|
2051
|
-
{ command: '/stat' }
|
|
2052
|
-
]
|
|
2053
|
-
};
|
|
2054
|
-
const shortcuts = toolShortcuts[toolKey] || defaultShortcuts;
|
|
2055
|
-
const shortcutGroup = document.querySelector('[data-role="tool-shortcuts"]');
|
|
2056
|
-
if (!shortcutGroup) return;
|
|
2057
|
-
|
|
2058
|
-
shortcutGroup.style.gridTemplateColumns = `repeat(${shortcuts.length}, minmax(0, 1fr))`;
|
|
2059
|
-
shortcutGroup.replaceChildren(...shortcuts.map(shortcut => {
|
|
2060
|
-
const button = document.createElement(shortcut.command ? 'button' : 'div');
|
|
2061
|
-
button.className = shortcut.command
|
|
2062
|
-
? 'command-key'
|
|
2063
|
-
: `key-btn${shortcut.special ? ' special-key' : ''}`;
|
|
2064
|
-
if (shortcut.command) {
|
|
2065
|
-
button.dataset.command = shortcut.command;
|
|
2066
|
-
button.textContent = shortcut.command;
|
|
2067
|
-
button.title = `Send ${shortcut.command}`;
|
|
2068
|
-
} else {
|
|
2069
|
-
button.dataset.key = shortcut.key;
|
|
2070
|
-
button.textContent = shortcut.label;
|
|
2071
|
-
button.title = shortcut.title || '';
|
|
2072
|
-
}
|
|
2073
|
-
return button;
|
|
2074
|
-
}));
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
|
|
2078
|
-
function codexJson(value) {
|
|
2079
|
-
if (typeof value === 'string') return value;
|
|
2080
|
-
try { return JSON.stringify(value || {}, null, 2); } catch (_) { return String(value || ''); }
|
|
2081
|
-
}
|
|
2082
|
-
function codexToolStatus(item) {
|
|
2083
|
-
const status = item.toolStatus || 'running';
|
|
2084
|
-
if (status === 'inProgress') return 'running';
|
|
2085
|
-
if (status === 'failed' || status === 'declined') return status;
|
|
2086
|
-
if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
|
|
2087
|
-
return status;
|
|
2088
|
-
}
|
|
2089
|
-
function formatCodexDuration(durationMs) {
|
|
2090
|
-
const value = Number(durationMs || 0);
|
|
2091
|
-
if (!(value > 0)) return '';
|
|
2092
|
-
if (value < 1000) return `${Math.round(value)}ms`;
|
|
2093
|
-
const seconds = value / 1000;
|
|
2094
|
-
if (seconds < 10) return `${seconds.toFixed(1).replace(/\.0$/, '')}s`;
|
|
2095
|
-
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
2096
|
-
const minutes = Math.floor(seconds / 60);
|
|
2097
|
-
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
|
2098
|
-
}
|
|
2099
|
-
function renderCodexDiff(diff) {
|
|
2100
|
-
return `<div class="codex-diff">${String(diff || '').split('\n').map(line => {
|
|
2101
|
-
const type = line.startsWith('+++') || line.startsWith('---') ? 'hunk' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'del' : line.startsWith('@@') ? 'hunk' : '';
|
|
2102
|
-
return `<div class="codex-diff-line ${type}">${escapeHtml(line || ' ')}</div>`;
|
|
2103
|
-
}).join('')}</div>`;
|
|
2104
|
-
}
|
|
2105
|
-
function normalizeCodexChanges(changes) {
|
|
2106
|
-
if (Array.isArray(changes)) return changes.filter(Boolean).map(change => [change.path || 'File', change]);
|
|
2107
|
-
if (changes && typeof changes === 'object') return Object.entries(changes);
|
|
2108
|
-
return [];
|
|
2109
|
-
}
|
|
2110
|
-
function codexChangeDiff(change = {}) {
|
|
2111
|
-
if (typeof change.diff === 'string') return change.diff;
|
|
2112
|
-
if (typeof change.unified_diff === 'string') return change.unified_diff;
|
|
2113
|
-
const kind = change.kind && typeof change.kind === 'object' ? change.kind : {};
|
|
2114
|
-
const type = change.type || kind.type || '';
|
|
2115
|
-
const oldText = change.modify?.old_content ?? change.old_content ?? change.oldContent ?? (type === 'delete' ? change.content : change.delete?.content) ?? '';
|
|
2116
|
-
const newText = change.modify?.new_content ?? change.new_content ?? change.newContent ?? (type === 'add' ? change.content : change.add?.content) ?? '';
|
|
2117
|
-
if (!oldText && !newText) return '';
|
|
2118
|
-
return `${String(oldText).split('\n').map(line => `-${line}`).join('\n')}\n${String(newText).split('\n').map(line => `+${line}`).join('\n')}`;
|
|
2119
|
-
}
|
|
2120
|
-
function renderCodexPatch(item) {
|
|
2121
|
-
const entries = normalizeCodexChanges(item.changes || item.input?.changes);
|
|
2122
|
-
if (!entries.length) return `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(codexJson(item.input))}</pre></div>`;
|
|
2123
|
-
return entries.map(([path, change]) => {
|
|
2124
|
-
const kind = change?.type || change?.kind?.type || 'edit';
|
|
2125
|
-
const move = change?.move_path || change?.kind?.move_path;
|
|
2126
|
-
const diff = codexChangeDiff(change);
|
|
2127
|
-
return `<details class="codex-patch-file"><summary><span class="path">${escapeHtml(path)}${move ? ` → ${escapeHtml(move)}` : ''}</span><span class="codex-patch-kind">${escapeHtml(kind)}</span></summary>${diff ? renderCodexDiff(diff) : ''}</details>`;
|
|
2128
|
-
}).join('');
|
|
2129
|
-
}
|
|
2130
|
-
function renderCodexPermission(request, inline = false) {
|
|
2131
|
-
if (!request) return '';
|
|
2132
|
-
const pending = request.status === 'pending';
|
|
2133
|
-
const labels = { approved: 'Allowed once', approved_for_session: 'Allowed for session', denied: 'Denied', abort: 'Stopped' };
|
|
2134
|
-
const detail = request.reason || (request.input && Object.keys(request.input).length ? codexJson(request.input) : '');
|
|
2135
|
-
const content = `${inline ? `<div class="codex-inline-permission-title">${escapeHtml(request.title || 'Permission required')}</div>` : ''}${detail ? `<div>${codexText(detail)}</div>` : ''}${pending ? `<div class="claude-permission-actions"><button class="small-btn primary" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'approved')">Yes</button><button class="small-btn" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'approved_for_session')">Yes, for session</button><button class="small-btn danger" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'abort')">Stop and explain</button></div>` : `<div class="codex-permission-result">${escapeHtml(labels[request.decision] || request.status)}</div>`}`;
|
|
2136
|
-
if (inline) return `<div class="codex-inline-permission">${content}</div>`;
|
|
2137
|
-
return `<div class="claude-tool claude-permission"><div class="claude-tool-header"><strong>${escapeHtml(request.title || 'Permission required')}</strong></div><div class="claude-tool-body">${content}</div></div>`;
|
|
2138
|
-
}
|
|
2139
|
-
function formatCodexReset(timestamp) {
|
|
2140
|
-
const value = Number(timestamp || 0);
|
|
2141
|
-
return value ? new Date(value * 1000).toLocaleString() : '';
|
|
2142
|
-
}
|
|
2143
|
-
function formatCodexTokens(value) {
|
|
2144
|
-
const count = Number(value || 0);
|
|
2145
|
-
return count >= 1000000 ? `${(count / 1000000).toFixed(1)}M`
|
|
2146
|
-
: count >= 1000 ? `${Math.round(count / 1000)}K` : String(count);
|
|
2147
|
-
}
|
|
2148
|
-
function codexStatusItem(label, value) {
|
|
2149
|
-
if (value == null || value === '') return '';
|
|
2150
|
-
return `<div class="codex-status-item"><div class="codex-status-label">${escapeHtml(label)}</div><div class="codex-status-value">${escapeHtml(value)}</div></div>`;
|
|
2151
|
-
}
|
|
2152
|
-
function renderCodexStatus(item) {
|
|
2153
|
-
const account = item.account || {};
|
|
2154
|
-
const limit = item.rateLimits || {};
|
|
2155
|
-
const primary = limit.primary;
|
|
2156
|
-
const secondary = limit.secondary;
|
|
2157
|
-
const context = item.context;
|
|
2158
|
-
const accountLabel = account.type === 'chatgpt'
|
|
2159
|
-
? [account.email, account.planType].filter(Boolean).join(' · ')
|
|
2160
|
-
: account.type === 'apiKey' ? 'API key' : account.type || 'Not signed in';
|
|
2161
|
-
const fiveHour = primary ? `${Math.max(0, 100 - Number(primary.usedPercent || 0))}% left${primary.resetsAt ? ` · resets ${formatCodexReset(primary.resetsAt)}` : ''}` : '';
|
|
2162
|
-
const weekly = secondary ? `${Math.max(0, 100 - Number(secondary.usedPercent || 0))}% left${secondary.resetsAt ? ` · resets ${formatCodexReset(secondary.resetsAt)}` : ''}` : '';
|
|
2163
|
-
const contextLabel = context ? `${context.remainingPercent}% left${context.contextWindow ? ` · ${formatCodexTokens(context.remainingTokens)} / ${formatCodexTokens(context.contextWindow)}` : ''}` : 'Available after the first usage update';
|
|
2164
|
-
return `<div class="codex-status-card"><div class="codex-status-title">${escapeHtml(item.title || 'Codex status')}</div><div class="codex-status-grid">
|
|
2165
|
-
${codexStatusItem('Account', accountLabel)}
|
|
2166
|
-
${codexStatusItem('Model', [item.model, item.effort].filter(Boolean).join(' · '))}
|
|
2167
|
-
${codexStatusItem('5h limit', fiveHour)}
|
|
2168
|
-
${codexStatusItem('Weekly limit', weekly)}
|
|
2169
|
-
${codexStatusItem('Context', contextLabel)}
|
|
2170
|
-
</div></div>`;
|
|
2171
|
-
}
|
|
2172
|
-
function renderCodexTool(item, permission = null) {
|
|
2173
|
-
const status = codexToolStatus(item);
|
|
2174
|
-
const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
|
|
2175
|
-
const runningClass = status === 'running' ? ' running' : '';
|
|
2176
|
-
if (item.name === 'CodexPatch') {
|
|
2177
|
-
return `<div class="codex-tool${isError ? ' error' : ''}">${renderCodexPatch(item)}${permission ? renderCodexPermission(permission, true) : ''}</div>`;
|
|
2178
|
-
}
|
|
2179
|
-
const command = item.name === 'CodexBash' ? item.command : item.title || item.tool || '';
|
|
2180
|
-
const icon = item.name === 'CodexBash' ? '>_' : item.name === 'McpTool' ? 'MCP' : item.name === 'Agent' ? 'A' : '•';
|
|
2181
|
-
const title = item.name === 'CodexBash' ? 'Command' : item.title || item.name || 'Tool';
|
|
2182
|
-
const hasInput = item.input && typeof item.input === 'object' && Object.keys(item.input).length > 0;
|
|
2183
|
-
const result = item.result || ((item.name === 'McpTool' || item.name === 'Agent') && hasInput ? codexJson(item.input) : '');
|
|
2184
|
-
const duration = status === 'running' ? '' : formatCodexDuration(item.durationMs);
|
|
2185
|
-
return `<details class="codex-tool${isError ? ' error' : ''}" data-codex-key="tool-${escapeHtml(item.id || item.providerId || '')}"><summary class="codex-tool-header"><span class="codex-tool-icon">${escapeHtml(icon)}</span><span class="codex-tool-title">${escapeHtml(title)}</span>${command && command !== title ? `<span class="codex-tool-command">${escapeHtml(command)}</span>` : '<span class="codex-tool-command"></span>'}${duration ? `<span class="codex-tool-duration">${escapeHtml(duration)}</span>` : ''}<span class="codex-tool-state${runningClass}">${escapeHtml(status === 'completed' ? '' : status)}</span></summary>${result ? `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(result)}</pre></div>` : ''}${permission ? renderCodexPermission(permission, true) : ''}</details>`;
|
|
2186
|
-
}
|
|
2187
|
-
function renderCodexToolGroup(items, permissionById, usedPermissions, turnEnd = null) {
|
|
2188
|
-
const running = items.some(item => codexToolStatus(item) === 'running');
|
|
2189
|
-
const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
|
|
2190
|
-
const startedAt = Math.min(...items.map(item => Number(item.startedAtMs || item.createdAt || Date.now())));
|
|
2191
|
-
const itemCompletedAt = Math.max(...items.map(item => Number(item.completedAtMs || 0)));
|
|
2192
|
-
const storedDuration = Number(turnEnd?.durationMs || 0);
|
|
2193
|
-
const completedAt = Number(turnEnd?.createdAt || 0);
|
|
2194
|
-
const durationMs = itemCompletedAt >= startedAt ? itemCompletedAt - startedAt : storedDuration > 0 ? storedDuration
|
|
2195
|
-
: (!running && completedAt >= startedAt ? completedAt - startedAt : 0);
|
|
2196
|
-
const duration = formatCodexDuration(durationMs);
|
|
2197
|
-
const tools = items.map(item => {
|
|
2198
|
-
const permission = permissionById.get(String(item.providerId || ''));
|
|
2199
|
-
if (permission) usedPermissions.add(permission.id);
|
|
2200
|
-
return renderCodexTool(item, permission);
|
|
2201
|
-
}).join('');
|
|
2202
|
-
const label = running ? 'Working' : failed ? 'Work finished with errors' : duration ? `Worked for ${duration}` : 'Worked';
|
|
2203
|
-
const key = items.map(item => item.id || item.providerId || '').join('-');
|
|
2204
|
-
return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"><summary>${label} · ${items.length} ${items.length === 1 ? 'tool' : 'tools'}</summary><div class="codex-work-group-body">${tools}</div></details>`;
|
|
2205
|
-
}
|
|
2206
|
-
function renderCodexMessageTime(item, finalOnly = false) {
|
|
2207
|
-
if (!item || (finalOnly && item.streaming)) return '';
|
|
2208
|
-
const timestamp = Number(finalOnly ? (item.completedAtMs || item.updatedAt || item.createdAt) : item.createdAt);
|
|
2209
|
-
if (!Number.isFinite(timestamp) || timestamp <= 0) return '';
|
|
2210
|
-
const date = new Date(timestamp);
|
|
2211
|
-
if (Number.isNaN(date.getTime())) return '';
|
|
2212
|
-
const label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2213
|
-
return `<time class="codex-message-time" datetime="${escapeHtml(date.toISOString())}" title="${escapeHtml(date.toLocaleString())}">${escapeHtml(label)}</time>`;
|
|
2214
|
-
}
|
|
2215
|
-
function syncCodexDom(current, next) {
|
|
2216
|
-
if (!current || !next) return;
|
|
2217
|
-
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
|
|
2218
|
-
current.replaceWith(next.cloneNode(true));
|
|
2219
|
-
return;
|
|
2220
|
-
}
|
|
2221
|
-
if (current.nodeType === Node.TEXT_NODE) {
|
|
2222
|
-
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
|
|
2223
|
-
return;
|
|
2224
|
-
}
|
|
2225
|
-
const currentKey = current.getAttribute?.('data-codex-key');
|
|
2226
|
-
const nextKey = next.getAttribute?.('data-codex-key');
|
|
2227
|
-
if (currentKey && nextKey && currentKey !== nextKey) {
|
|
2228
|
-
current.replaceWith(next.cloneNode(true));
|
|
2229
|
-
return;
|
|
2230
|
-
}
|
|
2231
|
-
const preserveOpen = current.tagName === 'DETAILS' && currentKey === nextKey;
|
|
2232
|
-
const wasOpen = preserveOpen ? current.open : false;
|
|
2233
|
-
for (const attribute of Array.from(current.attributes || [])) {
|
|
2234
|
-
if (!next.hasAttribute(attribute.name) && !(preserveOpen && attribute.name === 'open')) current.removeAttribute(attribute.name);
|
|
2235
|
-
}
|
|
2236
|
-
for (const attribute of Array.from(next.attributes || [])) {
|
|
2237
|
-
if (!(preserveOpen && attribute.name === 'open') && current.getAttribute(attribute.name) !== attribute.value) {
|
|
2238
|
-
current.setAttribute(attribute.name, attribute.value);
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
if (preserveOpen) current.open = wasOpen;
|
|
2242
|
-
const currentChildren = Array.from(current.childNodes);
|
|
2243
|
-
const nextChildren = Array.from(next.childNodes);
|
|
2244
|
-
const shared = Math.min(currentChildren.length, nextChildren.length);
|
|
2245
|
-
for (let i = 0; i < shared; i++) syncCodexDom(currentChildren[i], nextChildren[i]);
|
|
2246
|
-
for (let i = current.childNodes.length - 1; i >= nextChildren.length; i--) current.childNodes[i].remove();
|
|
2247
|
-
for (let i = shared; i < nextChildren.length; i++) current.appendChild(nextChildren[i].cloneNode(true));
|
|
2248
|
-
}
|
|
2249
|
-
function renderCodexChat() {
|
|
2250
|
-
if (codexRenderFrame != null) return;
|
|
2251
|
-
codexRenderFrame = requestAnimationFrame(() => {
|
|
2252
|
-
codexRenderFrame = null;
|
|
2253
|
-
commitCodexChatRender();
|
|
2254
|
-
});
|
|
2255
|
-
}
|
|
2256
|
-
function commitCodexChatRender() {
|
|
2257
|
-
const container = document.getElementById('codex-chat-container');
|
|
2258
|
-
if (!container) return;
|
|
2259
|
-
const wasEmpty = !container.firstElementChild;
|
|
2260
|
-
const previousScrollTop = container.scrollTop;
|
|
2261
|
-
const distanceFromBottom = container.scrollHeight - container.clientHeight - previousScrollTop;
|
|
2262
|
-
const shouldStickToBottom = wasEmpty || distanceFromBottom <= 64;
|
|
2263
|
-
const parts = [];
|
|
2264
|
-
const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
|
|
2265
|
-
const usedPermissions = new Set();
|
|
2266
|
-
const turnEndById = new Map(codexMessages.filter(item => item.kind === 'turn-end' && item.turnId)
|
|
2267
|
-
.map(item => [String(item.turnId), item]));
|
|
2268
|
-
const visible = codexMessages.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
|
|
2269
|
-
for (let i = 0; i < visible.length;) {
|
|
2270
|
-
const item = visible[i];
|
|
2271
|
-
if (item.kind === 'tool') {
|
|
2272
|
-
const tools = [];
|
|
2273
|
-
const turnId = item.turnId;
|
|
2274
|
-
while (i < visible.length && visible[i].kind === 'tool' && visible[i].turnId === turnId) tools.push(visible[i++]);
|
|
2275
|
-
parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions,
|
|
2276
|
-
turnEndById.get(String(turnId || ''))));
|
|
2277
|
-
continue;
|
|
2278
|
-
}
|
|
2279
|
-
if (item.kind === 'assistant') parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageTime(item, true)}</div>`);
|
|
2280
|
-
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageTime(item)}</div>`);
|
|
2281
|
-
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
2282
|
-
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
2283
|
-
i += 1;
|
|
2284
|
-
}
|
|
2285
|
-
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
2286
|
-
const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
|
|
2287
|
-
const template = document.createElement('template');
|
|
2288
|
-
template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
|
|
2289
|
-
const next = template.content.firstElementChild;
|
|
2290
|
-
const current = container.firstElementChild;
|
|
2291
|
-
if (!current) container.appendChild(next);
|
|
2292
|
-
else syncCodexDom(current, next);
|
|
2293
|
-
container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
function applyCodexState(state = {}) {
|
|
2297
|
-
const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
|
|
2298
|
-
codexState = { ...codexState, ...state };
|
|
2299
|
-
const permission = document.getElementById('codex-permission-select');
|
|
2300
|
-
if (permission) {
|
|
2301
|
-
const defaultOption = permission.querySelector('option[value="default"]');
|
|
2302
|
-
const labels = { untrusted: 'Untrusted', 'on-request': 'On request', never: 'Never ask' };
|
|
2303
|
-
if (defaultOption) defaultOption.textContent = codexState.effectivePermissionMode
|
|
2304
|
-
? `Default (${labels[codexState.effectivePermissionMode] || codexState.effectivePermissionMode})`
|
|
2305
|
-
: 'Default';
|
|
2306
|
-
permission.value = codexState.permissionMode || 'default';
|
|
2307
|
-
}
|
|
2308
|
-
const sandbox = document.getElementById('codex-sandbox-select');
|
|
2309
|
-
if (sandbox) {
|
|
2310
|
-
const defaultOption = sandbox.querySelector('option[value="default"]');
|
|
2311
|
-
const labels = { 'read-only': 'Read only', 'workspace-write': 'Workspace write', 'danger-full-access': 'Full access' };
|
|
2312
|
-
if (defaultOption) defaultOption.textContent = codexState.effectiveSandboxMode
|
|
2313
|
-
? `Default (${labels[codexState.effectiveSandboxMode] || codexState.effectiveSandboxMode})`
|
|
2314
|
-
: 'Default';
|
|
2315
|
-
sandbox.value = codexState.sandboxMode || 'default';
|
|
2316
|
-
}
|
|
2317
|
-
const modelButton = document.getElementById('codex-model-btn');
|
|
2318
|
-
if (modelButton) modelButton.textContent = 'Model';
|
|
2319
|
-
const abort = document.getElementById('codex-abort-btn');
|
|
2320
|
-
if (abort) abort.disabled = !codexState.canAbort;
|
|
2321
|
-
const fork = document.getElementById('codex-fork-btn');
|
|
2322
|
-
if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle');
|
|
2323
|
-
const terminal = document.getElementById('codex-terminal-switch');
|
|
2324
|
-
if (terminal) { terminal.textContent = codexState.presentation === 'terminal' ? 'CHAT' : 'TERM'; terminal.disabled = codexState.presentation === 'structured' && !codexState.canSwitchToTerminal; terminal.title = codexState.presentation === 'terminal' ? 'Return to Codex chat' : 'Switch to Codex terminal'; }
|
|
2325
|
-
renderCodexStateBar();
|
|
2326
|
-
renderCodexModelPanel();
|
|
2327
|
-
if (presentationChanged) setClaudeModeEnabled(isClaudeSession());
|
|
2328
|
-
renderCodexChat();
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
function renderCodexStateBar() {
|
|
2332
|
-
const el = document.getElementById('codex-state-bar');
|
|
2333
|
-
if (!el) return;
|
|
2334
|
-
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
2335
|
-
const subagents = Number(codexState.activeSubagentCount || 0) || 0;
|
|
2336
|
-
const parts = [];
|
|
2337
|
-
if (pending || codexState.status === 'waiting_approval') parts.push(`<span class="claude-state-pill warn">${pending || 1} approval${pending === 1 ? '' : 's'}</span>`);
|
|
2338
|
-
if (subagents) parts.push(`<span class="claude-state-pill">${subagents} subagent${subagents === 1 ? '' : 's'} running</span>`);
|
|
2339
|
-
el.innerHTML = parts.join('');
|
|
2340
|
-
el.style.display = parts.length ? 'flex' : 'none';
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
function toggleCodexModelPanel() {
|
|
2344
|
-
codexModelPanelOpen = !codexModelPanelOpen;
|
|
2345
|
-
codexResumePanelOpen = false;
|
|
2346
|
-
codexForkPanelOpen = false;
|
|
2347
|
-
codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
|
|
2348
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2349
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
2350
|
-
renderCodexModelPanel();
|
|
2351
|
-
updateTerminalControlsHeight();
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
function renderCodexModelPanel() {
|
|
2355
|
-
const panel = document.getElementById('codex-model-panel');
|
|
2356
|
-
if (!panel) return;
|
|
2357
|
-
panel.classList.toggle('active', codexModelPanelOpen);
|
|
2358
|
-
if (!codexModelPanelOpen) { panel.innerHTML = ''; return; }
|
|
2359
|
-
const models = codexState.models || [];
|
|
2360
|
-
const candidate = models.find(item => item.id === codexModelCandidate) || models.find(item => item.id === codexState.model) || models[0];
|
|
2361
|
-
if (!candidate) { panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading models...</div>'; return; }
|
|
2362
|
-
codexModelCandidate = candidate.id;
|
|
2363
|
-
const efforts = candidate.efforts?.length ? candidate.efforts : [candidate.defaultEffort || 'medium'];
|
|
2364
|
-
panel.innerHTML = `<div class="codex-picker-column">${models.map(item => `<button class="codex-picker-option${item.id === candidate.id ? ' selected' : ''}" onclick="selectCodexModelCandidate(decodePathValue('${encodePathValue(item.id)}'))">${escapeHtml(item.label || item.id)}</button>`).join('')}</div><div class="codex-picker-column">${efforts.map(effort => `<button class="codex-picker-option${candidate.id === codexState.model && effort === codexState.effort ? ' selected' : ''}" onclick="chooseCodexModelEffort(decodePathValue('${encodePathValue(candidate.id)}'), decodePathValue('${encodePathValue(effort)}'))">${escapeHtml(effort)}</button>`).join('')}</div>`;
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
function selectCodexModelCandidate(model) { codexModelCandidate = model; renderCodexModelPanel(); }
|
|
2368
|
-
function chooseCodexModelEffort(model, effort) {
|
|
2369
|
-
codexModelPanelOpen = false;
|
|
2370
|
-
applyCodexState({ model, effort });
|
|
2371
|
-
sendCodexSettings({ model, effort });
|
|
2372
|
-
updateTerminalControlsHeight();
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
function applyCodexEvent(event) {
|
|
2376
|
-
if (!event) return;
|
|
2377
|
-
if (event.type === 'message' && event.message) codexMessages.push(event.message);
|
|
2378
|
-
else if (event.type === 'message-updated' && event.message) { const i = codexMessages.findIndex(item => item.id === event.message.id); if (i >= 0) codexMessages[i] = event.message; else codexMessages.push(event.message); }
|
|
2379
|
-
else if (event.type === 'history-reset') codexMessages = event.messages || [];
|
|
2380
|
-
else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
|
|
2381
|
-
else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
|
|
2382
|
-
if (event.state) applyCodexState(event.state); else renderCodexChat();
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
|
|
2386
|
-
function updateCodexSettingsFromControls() {
|
|
2387
|
-
const permissionMode = document.getElementById('codex-permission-select').value;
|
|
2388
|
-
const sandboxMode = document.getElementById('codex-sandbox-select').value;
|
|
2389
|
-
applyCodexState({ permissionMode, sandboxMode });
|
|
2390
|
-
sendCodexSettings({ permissionMode, sandboxMode });
|
|
2391
|
-
}
|
|
2392
|
-
function abortCodexSession() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-abort' })); }
|
|
2393
|
-
function requestCodexStatus() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-status' })); }
|
|
2394
|
-
function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
|
|
2395
|
-
async function toggleCodexResumePanel() {
|
|
2396
|
-
codexResumePanelOpen = !codexResumePanelOpen;
|
|
2397
|
-
codexModelPanelOpen = false;
|
|
2398
|
-
codexForkPanelOpen = false;
|
|
2399
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
2400
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
2401
|
-
const panel = document.getElementById('codex-resume-panel');
|
|
2402
|
-
panel.classList.toggle('active', codexResumePanelOpen);
|
|
2403
|
-
updateTerminalControlsHeight();
|
|
2404
|
-
if (!codexResumePanelOpen) return;
|
|
2405
|
-
await loadCodexThreadPanel(panel, 'resume');
|
|
2406
|
-
}
|
|
2407
|
-
async function toggleCodexForkPanel() {
|
|
2408
|
-
if (!(codexState.presentation === 'structured' && codexState.status === 'idle')) return;
|
|
2409
|
-
codexForkPanelOpen = !codexForkPanelOpen;
|
|
2410
|
-
codexModelPanelOpen = false;
|
|
2411
|
-
codexResumePanelOpen = false;
|
|
2412
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
2413
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2414
|
-
const panel = document.getElementById('codex-fork-panel');
|
|
2415
|
-
panel.classList.toggle('active', codexForkPanelOpen);
|
|
2416
|
-
updateTerminalControlsHeight();
|
|
2417
|
-
if (!codexForkPanelOpen) return;
|
|
2418
|
-
await loadCodexThreadPanel(panel, 'fork');
|
|
2419
|
-
}
|
|
2420
|
-
async function loadCodexThreadPanel(panel, action) {
|
|
2421
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
|
|
2422
|
-
try {
|
|
2423
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume-threads`, {}, 30000);
|
|
2424
|
-
const data = await res.json();
|
|
2425
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Codex sessions');
|
|
2426
|
-
const items = data.items || [];
|
|
2427
|
-
panel.innerHTML = items.length ? items.map(item => {
|
|
2428
|
-
const questions = Array.isArray(item.questions) ? item.questions : [];
|
|
2429
|
-
const handler = action === 'fork' ? 'selectCodexForkThread' : 'selectCodexResumeThread';
|
|
2430
|
-
return `<button class="claude-resume-item" onclick="${handler}(decodePathValue('${encodePathValue(item.id)}'))"><div class="claude-resume-title"><span>${escapeHtml((questions[0] || 'Codex session').slice(0, 120))}${item.current ? ' · current' : ''}</span><span>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '')}</span></div><div class="codex-resume-question-secondary">${escapeHtml((questions[1] || '').slice(0, 120))}</div></button>`;
|
|
2431
|
-
}).join('') : '<div class="claude-resume-meta" style="padding:12px;">No Codex sessions found for this folder.</div>';
|
|
2432
|
-
} catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
|
|
2433
|
-
updateTerminalControlsHeight();
|
|
2434
|
-
}
|
|
2435
|
-
async function selectCodexResumeThread(threadId) {
|
|
2436
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
|
|
2437
|
-
if (!res.ok) { alert((await res.json()).error || 'Unable to resume Codex thread'); return; }
|
|
2438
|
-
codexResumePanelOpen = false;
|
|
2439
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2440
|
-
updateTerminalControlsHeight();
|
|
2441
|
-
}
|
|
2442
|
-
async function selectCodexForkThread(threadId) {
|
|
2443
|
-
const panel = document.getElementById('codex-fork-panel');
|
|
2444
|
-
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Forking and switching this conversation...</div>';
|
|
2445
|
-
updateTerminalControlsHeight();
|
|
2446
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-fork`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 60000);
|
|
2447
|
-
const data = await res.json();
|
|
2448
|
-
if (!res.ok || !data.success) {
|
|
2449
|
-
panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(data.error || 'Unable to fork Codex thread')}</div>`;
|
|
2450
|
-
updateTerminalControlsHeight();
|
|
2451
|
-
return;
|
|
2452
|
-
}
|
|
2453
|
-
codexForkPanelOpen = false;
|
|
2454
|
-
panel.classList.remove('active');
|
|
2455
|
-
panel.innerHTML = '';
|
|
2456
|
-
updateTerminalControlsHeight();
|
|
2457
|
-
}
|
|
2458
|
-
async function toggleCodexPresentation() {
|
|
2459
|
-
const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
|
|
2460
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
|
|
2461
|
-
if (!res.ok) alert((await res.json()).error || 'Unable to switch Codex interface');
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
function joinSession(id, sessionName, toolKey = null) {
|
|
2465
|
-
if (typeof clearImageAttachments === 'function' && selectedImageAttachments.length) {
|
|
2466
|
-
void clearImageAttachments();
|
|
2467
|
-
}
|
|
2468
|
-
stopTimedInputTimers();
|
|
2469
|
-
activeSessionId = id;
|
|
2470
|
-
window.activeSessionId = id;
|
|
2471
|
-
activeToolKey = toolKey;
|
|
2472
|
-
clearTimeout(sessionPollTimer);
|
|
2473
|
-
sessionPollTimer = null;
|
|
2474
|
-
markCompletionRead(id);
|
|
2475
|
-
loadTimedInputs();
|
|
2476
|
-
document.getElementById('session-title').innerText = sessionName;
|
|
2477
|
-
updateToolShortcuts(activeToolKey);
|
|
2478
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
2479
|
-
document.getElementById('terminal-view').classList.add('active');
|
|
2480
|
-
setClaudeModeEnabled(isClaudeSession());
|
|
2481
|
-
if (currentSocket) { currentSocket.close(); currentSocket = null; }
|
|
2482
|
-
initTerminal(id);
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
function initTerminal(sessionId) {
|
|
2486
|
-
if (isClaudeSession()) {
|
|
2487
|
-
initClaudeSession(sessionId);
|
|
2488
|
-
return;
|
|
2489
|
-
}
|
|
2490
|
-
if (isCodexSession() && codexState.presentation === 'structured') {
|
|
2491
|
-
initCodexSession(sessionId);
|
|
2492
|
-
return;
|
|
2493
|
-
}
|
|
2494
|
-
if (typeof Terminal === 'undefined') {
|
|
2495
|
-
alert('Terminal library not loaded yet.');
|
|
2496
|
-
showLobby();
|
|
2497
|
-
return;
|
|
2498
|
-
}
|
|
2499
|
-
function isTerminalAutoResponse(data) {
|
|
2500
|
-
return /^\x1b\[\?[\d;]*c$/.test(data) || /^\x1b\[>[\d;]*c$/.test(data);
|
|
2501
|
-
}
|
|
2502
|
-
if (!term) {
|
|
2503
|
-
term = new Terminal({ theme: { background: '#000', foreground: '#fff', cursor: '#0f0' }, cursorBlink: true, fontSize: 14, cursorStyle: 'bar', scrollback: 10000 });
|
|
2504
|
-
fitAddon = new FitAddon.FitAddon();
|
|
2505
|
-
term.loadAddon(fitAddon);
|
|
2506
|
-
term.open(document.getElementById('terminal'));
|
|
2507
|
-
term.onData(data => {
|
|
2508
|
-
if (isTerminalAutoResponse(data)) return;
|
|
2509
|
-
let finalData = data;
|
|
2510
|
-
if (modifiers.ctrl && data.length === 1) {
|
|
2511
|
-
const code = data.toLowerCase().charCodeAt(0);
|
|
2512
|
-
if (code >= 97 && code <= 122) finalData = String.fromCharCode(code - 96);
|
|
2513
|
-
}
|
|
2514
|
-
sendWS(finalData);
|
|
2515
|
-
});
|
|
2516
|
-
} else { term.clear(); }
|
|
2517
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2518
|
-
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
2519
|
-
currentSocket.onopen = () => {
|
|
2520
|
-
syncLayout();
|
|
2521
|
-
setTimeout(() => logViewSnapshot('ws-open'), 300);
|
|
2522
|
-
};
|
|
2523
|
-
currentSocket.onmessage = (e) => {
|
|
2524
|
-
const msg = JSON.parse(e.data);
|
|
2525
|
-
if (msg.type === 'output') term.write(msg.data);
|
|
2526
|
-
if (msg.type === 'reset') term.reset();
|
|
2527
|
-
if (msg.type === 'codex-event' && isCodexSession()) {
|
|
2528
|
-
applyCodexEvent(msg.event);
|
|
2529
|
-
if (msg.event?.type === 'presentation' && msg.event.presentation === 'structured') {
|
|
2530
|
-
currentSocket.close();
|
|
2531
|
-
currentSocket = null;
|
|
2532
|
-
initCodexSession(sessionId);
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
if (msg.type === 'exit') showLobby();
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
async function initClaudeSession(sessionId) {
|
|
2540
|
-
claudeMessages = [];
|
|
2541
|
-
claudePendingPermissions = [];
|
|
2542
|
-
claudeStatus = 'idle';
|
|
2543
|
-
const runtimeConfig = await refreshClaudeRuntimeConfig();
|
|
2544
|
-
claudeState = {
|
|
2545
|
-
permissionMode: 'default',
|
|
2546
|
-
model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
|
|
2547
|
-
effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
|
|
2548
|
-
contextRemaining: null,
|
|
2549
|
-
latestUsage: null,
|
|
2550
|
-
claudeSessionId: null,
|
|
2551
|
-
resumeSessionId: null,
|
|
2552
|
-
canAbort: false,
|
|
2553
|
-
pendingPermissionCount: 0
|
|
2554
|
-
};
|
|
2555
|
-
claudeResumePanelOpen = false;
|
|
2556
|
-
claudePickerOpen = null;
|
|
2557
|
-
claudeResumeItemsLoaded = false;
|
|
2558
|
-
document.getElementById('claude-resume-panel').classList.remove('active');
|
|
2559
|
-
document.getElementById('claude-resume-panel').innerHTML = '';
|
|
2560
|
-
closeClaudePicker();
|
|
2561
|
-
updateTerminalControlsHeight();
|
|
2562
|
-
applyClaudeRuntimeConfig(runtimeConfig);
|
|
2563
|
-
applyClaudeState(claudeState);
|
|
2564
|
-
renderClaudeChat();
|
|
2565
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2566
|
-
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
2567
|
-
currentSocket.onopen = () => logViewSnapshot('claude-ws-open');
|
|
2568
|
-
currentSocket.onmessage = (e) => {
|
|
2569
|
-
const msg = JSON.parse(e.data);
|
|
2570
|
-
if (msg.type === 'claude-snapshot' && msg.snapshot) {
|
|
2571
|
-
claudeMessages = msg.snapshot.messages || [];
|
|
2572
|
-
claudePendingPermissions = msg.snapshot.pendingPermissions || [];
|
|
2573
|
-
claudeStatus = msg.snapshot.status || 'idle';
|
|
2574
|
-
applyClaudeState(msg.snapshot.state || {
|
|
2575
|
-
status: claudeStatus,
|
|
2576
|
-
pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
|
|
2577
|
-
canAbort: claudeStatus === 'thinking'
|
|
2578
|
-
});
|
|
2579
|
-
renderClaudeChat();
|
|
2580
|
-
}
|
|
2581
|
-
if (msg.type === 'claude-event') applyClaudeEvent(msg.event);
|
|
2582
|
-
if (msg.type === 'exit') showLobby();
|
|
2583
|
-
};
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
function initCodexSession(sessionId) {
|
|
2587
|
-
codexMessages = [];
|
|
2588
|
-
codexPendingPermissions = [];
|
|
2589
|
-
codexModelPanelOpen = false;
|
|
2590
|
-
codexModelCandidate = null;
|
|
2591
|
-
codexResumePanelOpen = false;
|
|
2592
|
-
codexForkPanelOpen = false;
|
|
2593
|
-
document.getElementById('codex-model-panel').classList.remove('active');
|
|
2594
|
-
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2595
|
-
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
2596
|
-
document.getElementById('codex-control-rail').scrollLeft = 0;
|
|
2597
|
-
codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
|
|
2598
|
-
setClaudeModeEnabled(false);
|
|
2599
|
-
applyCodexState(codexState);
|
|
2600
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
2601
|
-
currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
|
|
2602
|
-
currentSocket.onmessage = (e) => {
|
|
2603
|
-
const msg = JSON.parse(e.data);
|
|
2604
|
-
if (msg.type === 'codex-snapshot' && msg.snapshot) {
|
|
2605
|
-
codexMessages = msg.snapshot.messages || [];
|
|
2606
|
-
codexPendingPermissions = msg.snapshot.pendingPermissions || [];
|
|
2607
|
-
applyCodexState(msg.snapshot.state || {});
|
|
2608
|
-
}
|
|
2609
|
-
if (msg.type === 'codex-event') {
|
|
2610
|
-
applyCodexEvent(msg.event);
|
|
2611
|
-
if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
|
|
2612
|
-
currentSocket.close();
|
|
2613
|
-
currentSocket = null;
|
|
2614
|
-
initTerminal(sessionId);
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
if (msg.type === 'exit') showLobby();
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
function sendWS(data) {
|
|
2622
|
-
if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
let layoutTimer = null;
|
|
2626
|
-
let layoutKeepAtBottom = false;
|
|
2627
|
-
|
|
2628
|
-
function isTerminalAtBottom() {
|
|
2629
|
-
if (!term || !term.buffer || !term.buffer.active) return true;
|
|
2630
|
-
const active = term.buffer.active;
|
|
2631
|
-
return active.viewportY >= active.baseY - 1;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
function scrollTerminalViewportToBottom() {
|
|
2635
|
-
if (!term) return;
|
|
2636
|
-
term.scrollToBottom();
|
|
2637
|
-
const viewport = document.querySelector('#terminal .xterm-viewport');
|
|
2638
|
-
if (viewport) viewport.scrollTop = viewport.scrollHeight;
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
function restoreTerminalBottomSoon() {
|
|
2642
|
-
scrollTerminalViewportToBottom();
|
|
2643
|
-
requestAnimationFrame(() => {
|
|
2644
|
-
scrollTerminalViewportToBottom();
|
|
2645
|
-
requestAnimationFrame(scrollTerminalViewportToBottom);
|
|
2646
|
-
});
|
|
2647
|
-
setTimeout(scrollTerminalViewportToBottom, 120);
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
function syncLayout(options = {}) {
|
|
2651
|
-
if (!fitAddon || !currentSocket) return;
|
|
2652
|
-
const keepAtBottom = typeof options.keepAtBottom === 'boolean' ? options.keepAtBottom : isTerminalAtBottom();
|
|
2653
|
-
layoutKeepAtBottom = layoutKeepAtBottom || keepAtBottom;
|
|
2654
|
-
clearTimeout(layoutTimer);
|
|
2655
|
-
layoutTimer = setTimeout(() => {
|
|
2656
|
-
const shouldRestoreBottom = layoutKeepAtBottom;
|
|
2657
|
-
layoutKeepAtBottom = false;
|
|
2658
|
-
fitAddon.fit();
|
|
2659
|
-
if (shouldRestoreBottom) restoreTerminalBottomSoon();
|
|
2660
|
-
if (currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
2661
|
-
}, 50);
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
function previewText(text, maxChars = 320) {
|
|
2665
|
-
if (!text) return '';
|
|
2666
|
-
const normalized = String(text)
|
|
2667
|
-
.replace(/\r/g, '\\r')
|
|
2668
|
-
.replace(/\n/g, '\\n')
|
|
2669
|
-
.replace(/\t/g, '\\t')
|
|
2670
|
-
.replace(/\x1b/g, '\\x1b');
|
|
2671
|
-
return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
async function logClientDebug(event, payload) {
|
|
2675
|
-
try {
|
|
2676
|
-
await fetch('/api/debug/client-log', {
|
|
2677
|
-
method: 'POST',
|
|
2678
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2679
|
-
body: JSON.stringify({ sessionId: activeSessionId, event, payload })
|
|
2680
|
-
});
|
|
2681
|
-
} catch (_) {}
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
function snapshotTerminalBuffer(maxLines = 12) {
|
|
2685
|
-
if (!term || !term.buffer || !term.buffer.active) return null;
|
|
2686
|
-
const active = term.buffer.active;
|
|
2687
|
-
const totalLines = active.length || 0;
|
|
2688
|
-
const start = Math.max(0, totalLines - maxLines);
|
|
2689
|
-
const tailLines = [];
|
|
2690
|
-
for (let i = start; i < totalLines; i++) {
|
|
2691
|
-
const line = active.getLine(i);
|
|
2692
|
-
if (!line) continue;
|
|
2693
|
-
tailLines.push(line.translateToString(true));
|
|
2694
|
-
}
|
|
2695
|
-
return {
|
|
2696
|
-
cols: term.cols,
|
|
2697
|
-
rows: term.rows,
|
|
2698
|
-
baseY: active.baseY,
|
|
2699
|
-
viewportY: active.viewportY,
|
|
2700
|
-
cursorX: active.cursorX,
|
|
2701
|
-
cursorY: active.cursorY,
|
|
2702
|
-
totalLines,
|
|
2703
|
-
tailPreview: previewText(tailLines.join('\n'))
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
function logViewSnapshot(event) {
|
|
2708
|
-
logClientDebug(event, {
|
|
2709
|
-
sessionId: activeSessionId,
|
|
2710
|
-
terminalSnapshot: snapshotTerminalBuffer()
|
|
2711
|
-
});
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
// Navigation
|
|
2715
|
-
function showTerminal() {
|
|
2716
|
-
clearTimeout(sessionPollTimer);
|
|
2717
|
-
sessionPollTimer = null;
|
|
2718
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
2719
|
-
document.getElementById('terminal-view').classList.add('active');
|
|
2720
|
-
logViewSnapshot('show-terminal');
|
|
2721
|
-
syncLayout();
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
function showGitPreview() {
|
|
2725
|
-
clearTimeout(sessionPollTimer);
|
|
2726
|
-
sessionPollTimer = null;
|
|
2727
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
2728
|
-
document.getElementById('git-view').classList.add('active');
|
|
2729
|
-
switchGitTab('changes');
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
let historyWrapEnabled = true;
|
|
2733
|
-
|
|
2734
|
-
function showHistory() {
|
|
2735
|
-
clearTimeout(sessionPollTimer);
|
|
2736
|
-
sessionPollTimer = null;
|
|
2737
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
2738
|
-
document.getElementById('history-view').classList.add('active');
|
|
2739
|
-
logViewSnapshot('show-history-before-load');
|
|
2740
|
-
loadHistory();
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
function toggleHistoryWrap() {
|
|
2744
|
-
historyWrapEnabled = !historyWrapEnabled;
|
|
2745
|
-
const contentEl = document.getElementById('history-content');
|
|
2746
|
-
contentEl.classList.toggle('nowrap', !historyWrapEnabled);
|
|
2747
|
-
document.getElementById('history-wrap-btn').style.color = historyWrapEnabled ? 'var(--primary)' : 'var(--text-dim)';
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
async function loadHistory() {
|
|
2751
|
-
const contentEl = document.getElementById('history-content');
|
|
2752
|
-
const metaEl = document.getElementById('history-meta');
|
|
2753
|
-
if (!activeSessionId) {
|
|
2754
|
-
metaEl.textContent = 'No active session.';
|
|
2755
|
-
contentEl.textContent = '';
|
|
2756
|
-
return;
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
metaEl.textContent = 'Loading...';
|
|
2760
|
-
try {
|
|
2761
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/history`, {}, 30000);
|
|
2762
|
-
const data = await res.json();
|
|
2763
|
-
if (!data.success) throw new Error(data.error || 'Failed to load history');
|
|
2764
|
-
|
|
2765
|
-
const updatedAt = data.updatedAt ? new Date(data.updatedAt).toLocaleTimeString() : 'unknown';
|
|
2766
|
-
const size = typeof data.bytes === 'number' ? `${Math.round(data.bytes / 1024)} KB` : 'unknown size';
|
|
2767
|
-
const truncated = data.truncated ? ' | truncated' : '';
|
|
2768
|
-
metaEl.textContent = `${data.tool || 'Session'} | ${data.lines || 0} lines | ${size} | updated ${updatedAt}${truncated}`;
|
|
2769
|
-
contentEl.textContent = data.text || 'No readable history yet.';
|
|
2770
|
-
contentEl.classList.toggle('nowrap', !historyWrapEnabled);
|
|
2771
|
-
document.getElementById('history-wrap-btn').style.color = historyWrapEnabled ? 'var(--primary)' : 'var(--text-dim)';
|
|
2772
|
-
logClientDebug('history-loaded', {
|
|
2773
|
-
meta: metaEl.textContent,
|
|
2774
|
-
historyLines: data.lines || 0,
|
|
2775
|
-
historyBytes: data.bytes || 0,
|
|
2776
|
-
historyTail: previewText(data.text || '')
|
|
2777
|
-
});
|
|
2778
|
-
requestAnimationFrame(() => {
|
|
2779
|
-
contentEl.scrollTop = contentEl.scrollHeight;
|
|
2780
|
-
});
|
|
2781
|
-
} catch (e) {
|
|
2782
|
-
metaEl.textContent = 'Failed to load history.';
|
|
2783
|
-
contentEl.textContent = e.message;
|
|
2784
|
-
logClientDebug('history-load-failed', { message: e.message });
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
async function copyHistoryContent() {
|
|
2789
|
-
const contentEl = document.getElementById('history-content');
|
|
2790
|
-
const text = contentEl.textContent || '';
|
|
2791
|
-
if (!text || text === 'Loading...' || text === 'No readable history yet.') {
|
|
2792
|
-
alert('No history to copy.');
|
|
2793
|
-
return;
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
try {
|
|
2797
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
2798
|
-
await navigator.clipboard.writeText(text);
|
|
2799
|
-
} else {
|
|
2800
|
-
const range = document.createRange();
|
|
2801
|
-
range.selectNode(contentEl);
|
|
2802
|
-
window.getSelection().removeAllRanges();
|
|
2803
|
-
window.getSelection().addRange(range);
|
|
2804
|
-
document.execCommand('copy');
|
|
2805
|
-
window.getSelection().removeAllRanges();
|
|
2806
|
-
}
|
|
2807
|
-
alert('History copied.');
|
|
2808
|
-
} catch (e) {
|
|
2809
|
-
alert('Copy failed. You can select the history text manually.');
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
const inputEl = document.getElementById('cmd-input');
|
|
2814
|
-
const imageFileInput = document.getElementById('image-file-input');
|
|
2815
|
-
const attachmentStrip = document.getElementById('attachment-strip');
|
|
2816
|
-
let selectedImageAttachments = [];
|
|
2817
|
-
let keepTerminalBottomForNextInput = false;
|
|
2818
|
-
|
|
2819
|
-
function isCodexImageAttachmentAvailable() {
|
|
2820
|
-
return isCodexSession() && codexState.presentation === 'structured';
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
function syncComposerButtonState() {
|
|
2824
|
-
const menuOpen = document.getElementById('composer-menu').classList.contains('active');
|
|
2825
|
-
const timerOpen = document.getElementById('timed-send-panel').classList.contains('active');
|
|
2826
|
-
document.getElementById('timer-btn').classList.toggle('active', menuOpen || timerOpen);
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
function renderImageAttachments() {
|
|
2830
|
-
attachmentStrip.innerHTML = selectedImageAttachments.map(item => (
|
|
2831
|
-
`<div class="attachment-chip${item.uploading ? ' uploading' : ''}"><span aria-hidden="true">▧</span><span class="attachment-chip-content"><span class="attachment-chip-name" title="${escapeHtml(item.name)}">${escapeHtml(item.name)}</span>${item.uploading ? `<span class="attachment-progress${item.progressKnown ? '' : ' estimated'}"><span style="width:${Math.max(0, Math.min(100, item.progress || 0))}%"></span></span><span class="attachment-status">${escapeHtml(item.status || (item.progressKnown ? `Uploading ${Math.round(item.progress || 0)}%` : 'Uploading original image…'))}</span>` : ''}</span><button class="attachment-remove" type="button" title="Remove image" aria-label="Remove ${escapeHtml(item.name)}" onclick="removeImageAttachment('${item.id}')">×</button></div>`
|
|
2832
|
-
)).join('');
|
|
2833
|
-
attachmentStrip.classList.toggle('active', selectedImageAttachments.length > 0);
|
|
2834
|
-
updateTerminalControlsHeight();
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
window.removeImageAttachment = async function(attachmentId) {
|
|
2838
|
-
const attachment = selectedImageAttachments.find(item => item.id === attachmentId);
|
|
2839
|
-
selectedImageAttachments = selectedImageAttachments.filter(item => item.id !== attachmentId);
|
|
2840
|
-
renderImageAttachments();
|
|
2841
|
-
if (!attachment) return;
|
|
2842
|
-
clearInterval(attachment.indicatorTimer);
|
|
2843
|
-
attachment.abortUpload?.();
|
|
2844
|
-
if (attachment.uploading) return;
|
|
2845
|
-
try {
|
|
2846
|
-
await fetchWithTimeout(`/api/sessions/${attachment.sessionId}/attachments/images/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
|
|
2847
|
-
} catch (_) {
|
|
2848
|
-
// The server also removes all attachments when the session ends.
|
|
2849
|
-
}
|
|
2850
|
-
};
|
|
2851
|
-
|
|
2852
|
-
async function clearImageAttachments() {
|
|
2853
|
-
const pending = selectedImageAttachments;
|
|
2854
|
-
selectedImageAttachments = [];
|
|
2855
|
-
renderImageAttachments();
|
|
2856
|
-
for (const item of pending) item.abortUpload?.();
|
|
2857
|
-
await Promise.all(pending.filter(item => !item.uploading).map(item => fetchWithTimeout(
|
|
2858
|
-
`/api/sessions/${item.sessionId}/attachments/images/${encodeURIComponent(item.id)}`,
|
|
2859
|
-
{ method: 'DELETE' }
|
|
2860
|
-
).catch(() => null)));
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
const IMAGE_UPLOAD_CHUNK_BYTES = 512 * 1024;
|
|
2864
|
-
|
|
2865
|
-
function uploadImageInChunks(sessionId, file, onProgress) {
|
|
2866
|
-
let xhr = null;
|
|
2867
|
-
let cancelled = false;
|
|
2868
|
-
const uploadId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
2869
|
-
const chunkTotal = Math.ceil(file.size / IMAGE_UPLOAD_CHUNK_BYTES);
|
|
2870
|
-
return {
|
|
2871
|
-
abort: () => {
|
|
2872
|
-
cancelled = true;
|
|
2873
|
-
xhr?.abort();
|
|
2874
|
-
void fetch(`/api/sessions/${sessionId}/attachments/images/uploads/${encodeURIComponent(uploadId)}`, { method: 'DELETE' }).catch(() => null);
|
|
2875
|
-
},
|
|
2876
|
-
promise: (async () => {
|
|
2877
|
-
for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) {
|
|
2878
|
-
if (cancelled) throw new Error('Image upload cancelled');
|
|
2879
|
-
const start = chunkIndex * IMAGE_UPLOAD_CHUNK_BYTES;
|
|
2880
|
-
const chunk = file.slice(start, Math.min(file.size, start + IMAGE_UPLOAD_CHUNK_BYTES));
|
|
2881
|
-
const result = await new Promise((resolve, reject) => {
|
|
2882
|
-
xhr = new XMLHttpRequest();
|
|
2883
|
-
xhr.open('POST', `/api/sessions/${sessionId}/attachments/images/chunks`);
|
|
2884
|
-
xhr.timeout = 60_000;
|
|
2885
|
-
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
|
|
2886
|
-
xhr.setRequestHeader('X-Glad-Upload-Id', uploadId);
|
|
2887
|
-
xhr.setRequestHeader('X-Glad-Chunk-Index', String(chunkIndex));
|
|
2888
|
-
xhr.setRequestHeader('X-Glad-Chunk-Total', String(chunkTotal));
|
|
2889
|
-
xhr.onerror = () => reject(new Error('Network error while uploading image'));
|
|
2890
|
-
xhr.ontimeout = () => reject(new Error(`Image upload timed out on chunk ${chunkIndex + 1}/${chunkTotal}`));
|
|
2891
|
-
xhr.onabort = () => reject(new Error('Image upload cancelled'));
|
|
2892
|
-
xhr.onload = () => {
|
|
2893
|
-
let data = {};
|
|
2894
|
-
try { data = JSON.parse(xhr.responseText || '{}'); } catch (_) {}
|
|
2895
|
-
if (xhr.status < 200 || xhr.status >= 300 || !data.success) {
|
|
2896
|
-
reject(new Error(data.error || `Upload failed (HTTP ${xhr.status})`));
|
|
2897
|
-
return;
|
|
2898
|
-
}
|
|
2899
|
-
resolve(data);
|
|
2900
|
-
};
|
|
2901
|
-
xhr.send(chunk);
|
|
2902
|
-
});
|
|
2903
|
-
const confirmedBytes = Math.min(file.size, start + chunk.size);
|
|
2904
|
-
onProgress(Math.round((confirmedBytes / file.size) * 100), chunkIndex + 1, chunkTotal);
|
|
2905
|
-
if (result.complete) return result.attachment;
|
|
2906
|
-
}
|
|
2907
|
-
throw new Error('Image upload did not complete');
|
|
2908
|
-
})()
|
|
2909
|
-
};
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
async function uploadImageFiles(files) {
|
|
2913
|
-
if (!isCodexImageAttachmentAvailable()) {
|
|
2914
|
-
alert('Image attachments are available only in Codex chat mode.');
|
|
2915
|
-
return;
|
|
2916
|
-
}
|
|
2917
|
-
const remaining = 5 - selectedImageAttachments.length;
|
|
2918
|
-
const batch = Array.from(files).slice(0, remaining);
|
|
2919
|
-
if (files.length > remaining) alert('You can attach up to 5 images at a time.');
|
|
2920
|
-
for (const file of batch) {
|
|
2921
|
-
if (file.size > 50 * 1024 * 1024) {
|
|
2922
|
-
alert(`${file.name} is larger than 50 MB.`);
|
|
2923
|
-
continue;
|
|
2924
|
-
}
|
|
2925
|
-
const chunkTotal = Math.ceil(file.size / IMAGE_UPLOAD_CHUNK_BYTES);
|
|
2926
|
-
const pending = {
|
|
2927
|
-
id: `uploading-${crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`}`,
|
|
2928
|
-
name: file.name || 'image',
|
|
2929
|
-
sessionId: activeSessionId,
|
|
2930
|
-
uploading: true,
|
|
2931
|
-
progress: 0,
|
|
2932
|
-
progressKnown: true,
|
|
2933
|
-
status: '0%',
|
|
2934
|
-
abortUpload: null
|
|
2935
|
-
};
|
|
2936
|
-
selectedImageAttachments.push(pending);
|
|
2937
|
-
renderImageAttachments();
|
|
2938
|
-
try {
|
|
2939
|
-
const upload = uploadImageInChunks(activeSessionId, file, progress => {
|
|
2940
|
-
pending.progress = progress;
|
|
2941
|
-
pending.status = `${progress}%`;
|
|
2942
|
-
renderImageAttachments();
|
|
2943
|
-
});
|
|
2944
|
-
pending.abortUpload = upload.abort;
|
|
2945
|
-
const attachment = await upload.promise;
|
|
2946
|
-
const index = selectedImageAttachments.indexOf(pending);
|
|
2947
|
-
if (index < 0) {
|
|
2948
|
-
await fetchWithTimeout(`/api/sessions/${activeSessionId}/attachments/images/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
|
|
2949
|
-
continue;
|
|
2950
|
-
}
|
|
2951
|
-
selectedImageAttachments[index] = { ...attachment, name: file.name || attachment.name, sessionId: activeSessionId };
|
|
2952
|
-
renderImageAttachments();
|
|
2953
|
-
} catch (e) {
|
|
2954
|
-
selectedImageAttachments = selectedImageAttachments.filter(item => item !== pending);
|
|
2955
|
-
renderImageAttachments();
|
|
2956
|
-
if (e.message === 'Image upload cancelled') continue;
|
|
2957
|
-
alert(`Could not add ${file.name}: ${e.message}`);
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
|
|
2962
|
-
function closeComposerMenu() {
|
|
2963
|
-
document.getElementById('composer-menu').classList.remove('active');
|
|
2964
|
-
syncComposerButtonState();
|
|
2965
|
-
updateTerminalControlsHeight();
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
function openTimedSendPanel() {
|
|
2969
|
-
closeComposerMenu();
|
|
2970
|
-
if (isClaudeSession()) {
|
|
2971
|
-
closeClaudePicker();
|
|
2972
|
-
closeClaudeUsagePanel();
|
|
2973
|
-
claudeResumePanelOpen = false;
|
|
2974
|
-
document.getElementById('claude-resume-panel').classList.remove('active');
|
|
2975
|
-
}
|
|
2976
|
-
initTimedDelaySelectors();
|
|
2977
|
-
resetTimedEditor({ keepInput: true });
|
|
2978
|
-
document.getElementById('timed-send-panel').classList.add('active');
|
|
2979
|
-
updateTimedSendPreview();
|
|
2980
|
-
loadTimedInputs();
|
|
2981
|
-
syncComposerButtonState();
|
|
2982
|
-
updateTerminalControlsHeight();
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
function markInputEditStart() {
|
|
2986
|
-
keepTerminalBottomForNextInput = keepTerminalBottomForNextInput || isTerminalAtBottom();
|
|
2987
|
-
}
|
|
2988
|
-
|
|
2989
|
-
inputEl.addEventListener('beforeinput', markInputEditStart);
|
|
2990
|
-
inputEl.addEventListener('input', function() {
|
|
2991
|
-
const keepAtBottom = keepTerminalBottomForNextInput || isTerminalAtBottom();
|
|
2992
|
-
keepTerminalBottomForNextInput = false;
|
|
2993
|
-
this.style.height = 'auto';
|
|
2994
|
-
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
|
2995
|
-
if (keepAtBottom) restoreTerminalBottomSoon();
|
|
2996
|
-
});
|
|
2997
|
-
|
|
2998
|
-
function performSend() {
|
|
2999
|
-
const val = inputEl.value;
|
|
3000
|
-
const readyImageAttachments = selectedImageAttachments.filter(item => !item.uploading);
|
|
3001
|
-
if (selectedImageAttachments.some(item => item.uploading)) {
|
|
3002
|
-
alert('Wait for image uploads to finish before sending.');
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
3005
|
-
if (val && isClaudeSession()) {
|
|
3006
|
-
if (currentSocket && currentSocket.readyState === 1) {
|
|
3007
|
-
currentSocket.send(JSON.stringify({ type: 'claude-input', text: val }));
|
|
3008
|
-
}
|
|
3009
|
-
inputEl.value = '';
|
|
3010
|
-
inputEl.style.height = '38px';
|
|
3011
|
-
return;
|
|
3012
|
-
}
|
|
3013
|
-
if ((val || readyImageAttachments.length) && isCodexSession() && codexState.presentation === 'structured') {
|
|
3014
|
-
if (currentSocket && currentSocket.readyState === 1) {
|
|
3015
|
-
currentSocket.send(JSON.stringify({
|
|
3016
|
-
type: 'codex-input',
|
|
3017
|
-
text: val,
|
|
3018
|
-
attachmentIds: readyImageAttachments.map(item => item.id)
|
|
3019
|
-
}));
|
|
3020
|
-
}
|
|
3021
|
-
inputEl.value = '';
|
|
3022
|
-
inputEl.style.height = '38px';
|
|
3023
|
-
selectedImageAttachments = [];
|
|
3024
|
-
renderImageAttachments();
|
|
3025
|
-
return;
|
|
3026
|
-
}
|
|
3027
|
-
if (val) {
|
|
3028
|
-
const formattedVal = val.replace(/\n/g, '\r');
|
|
3029
|
-
sendWS(formattedVal);
|
|
3030
|
-
inputEl.value = '';
|
|
3031
|
-
inputEl.style.height = '38px';
|
|
3032
|
-
restoreTerminalBottomSoon();
|
|
3033
|
-
setTimeout(() => { sendWS('\r'); }, 1000);
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
|
-
let timedInputs = [];
|
|
3038
|
-
|
|
3039
|
-
function initTimedDelaySelectors() {
|
|
3040
|
-
const hours = document.getElementById('timed-hours');
|
|
3041
|
-
const minutes = document.getElementById('timed-minutes');
|
|
3042
|
-
if (hours.options.length) return;
|
|
3043
|
-
for (let i = 0; i <= 23; i++) {
|
|
3044
|
-
hours.add(new Option(`${i} hr`, String(i)));
|
|
3045
|
-
}
|
|
3046
|
-
for (let i = 0; i <= 59; i++) {
|
|
3047
|
-
minutes.add(new Option(`${i} min`, String(i)));
|
|
3048
|
-
}
|
|
3049
|
-
hours.value = '0';
|
|
3050
|
-
minutes.value = '5';
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
function getTimedDelayMs() {
|
|
3054
|
-
const hours = Number(document.getElementById('timed-hours').value);
|
|
3055
|
-
const minutes = Number(document.getElementById('timed-minutes').value);
|
|
3056
|
-
const delayMs = ((hours * 60) + minutes) * 60 * 1000;
|
|
3057
|
-
return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0;
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
function parseSendAt() {
|
|
3061
|
-
const delayMs = getTimedDelayMs();
|
|
3062
|
-
return delayMs > 0 ? Date.now() + delayMs : null;
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
function setTimedDelayFromMs(ms) {
|
|
3066
|
-
const totalMinutes = Math.max(1, Math.ceil(ms / 60000));
|
|
3067
|
-
const hours = Math.min(23, Math.floor(totalMinutes / 60));
|
|
3068
|
-
const minutes = Math.min(59, totalMinutes - hours * 60);
|
|
3069
|
-
document.getElementById('timed-hours').value = String(hours);
|
|
3070
|
-
document.getElementById('timed-minutes').value = String(minutes);
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
function updateTimedSendPreview() {
|
|
3074
|
-
const preview = document.getElementById('timed-preview');
|
|
3075
|
-
const sendAt = parseSendAt();
|
|
3076
|
-
preview.textContent = sendAt
|
|
3077
|
-
? `Will run at ${new Date(sendAt).toLocaleString()}`
|
|
3078
|
-
: 'Choose at least 1 minute.';
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
function formatCountdown(sendAt) {
|
|
3082
|
-
const remaining = Math.max(0, sendAt - Date.now());
|
|
3083
|
-
const totalSeconds = Math.ceil(remaining / 1000);
|
|
3084
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
3085
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
3086
|
-
const seconds = totalSeconds % 60;
|
|
3087
|
-
if (hours > 0) return `${hours}h\n${String(minutes).padStart(2, '0')}m`;
|
|
3088
|
-
return `${minutes}:${String(seconds).padStart(2, '0')}`;
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
function renderTimedTags() {
|
|
3092
|
-
const rail = document.getElementById('timed-tag-rail');
|
|
3093
|
-
const now = Date.now();
|
|
3094
|
-
const activeItems = timedInputs.filter(item => item.sendAt > now);
|
|
3095
|
-
rail.innerHTML = activeItems.map(item => {
|
|
3096
|
-
const encodedId = encodePathValue(item.id);
|
|
3097
|
-
return `<button class="timed-tag${item.id === editingTimedInputId ? ' active' : ''}" title="${escapeHtml(item.text)}" onclick="editTimedInput(decodePathValue('${encodedId}'))">${escapeHtml(formatCountdown(item.sendAt))}</button>`;
|
|
3098
|
-
}).join('');
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
function closeTimedSendPanel() {
|
|
3102
|
-
document.getElementById('timed-send-panel').classList.remove('active');
|
|
3103
|
-
editingTimedInputId = null;
|
|
3104
|
-
renderTimedTags();
|
|
3105
|
-
syncComposerButtonState();
|
|
3106
|
-
updateTerminalControlsHeight();
|
|
3107
|
-
}
|
|
3108
|
-
|
|
3109
|
-
function stopTimedInputTimers() {
|
|
3110
|
-
clearTimeout(timedSendRefreshTimer);
|
|
3111
|
-
timedSendRefreshTimer = null;
|
|
3112
|
-
if (timedTagTimer) clearInterval(timedTagTimer);
|
|
3113
|
-
timedTagTimer = null;
|
|
3114
|
-
timedInputs = [];
|
|
3115
|
-
renderTimedTags();
|
|
3116
|
-
}
|
|
3117
|
-
|
|
3118
|
-
function resetTimedEditor(options = {}) {
|
|
3119
|
-
editingTimedInputId = null;
|
|
3120
|
-
document.getElementById('timed-save-btn').textContent = 'Add Timer';
|
|
3121
|
-
document.getElementById('timed-cancel-edit-btn').style.display = 'none';
|
|
3122
|
-
document.getElementById('timed-delete-btn').style.display = 'none';
|
|
3123
|
-
if (!options.keepInput) {
|
|
3124
|
-
inputEl.value = '';
|
|
3125
|
-
inputEl.style.height = '38px';
|
|
3126
|
-
}
|
|
3127
|
-
setTimedDelayFromMs(5 * 60 * 1000);
|
|
3128
|
-
updateTimedSendPreview();
|
|
3129
|
-
renderTimedTags();
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
|
-
async function loadTimedInputs() {
|
|
3133
|
-
clearTimeout(timedSendRefreshTimer);
|
|
3134
|
-
timedSendRefreshTimer = null;
|
|
3135
|
-
if (!activeSessionId) return;
|
|
3136
|
-
|
|
3137
|
-
try {
|
|
3138
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/timed-inputs`);
|
|
3139
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
3140
|
-
const data = await res.json();
|
|
3141
|
-
timedInputs = data.items || [];
|
|
3142
|
-
renderTimedTags();
|
|
3143
|
-
if (!timedTagTimer) timedTagTimer = setInterval(renderTimedTags, 1000);
|
|
3144
|
-
timedSendRefreshTimer = setTimeout(loadTimedInputs, 15000);
|
|
3145
|
-
} catch (e) {
|
|
3146
|
-
timedInputs = [];
|
|
3147
|
-
renderTimedTags();
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
|
|
3151
|
-
function editTimedInput(id) {
|
|
3152
|
-
const item = timedInputs.find(value => value.id === id);
|
|
3153
|
-
if (!item) return;
|
|
3154
|
-
closeComposerMenu();
|
|
3155
|
-
initTimedDelaySelectors();
|
|
3156
|
-
editingTimedInputId = id;
|
|
3157
|
-
inputEl.value = item.text || '';
|
|
3158
|
-
inputEl.style.height = 'auto';
|
|
3159
|
-
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
|
3160
|
-
setTimedDelayFromMs(item.sendAt - Date.now());
|
|
3161
|
-
document.getElementById('timed-save-btn').textContent = 'Update Timer';
|
|
3162
|
-
document.getElementById('timed-cancel-edit-btn').style.display = '';
|
|
3163
|
-
document.getElementById('timed-delete-btn').style.display = '';
|
|
3164
|
-
document.getElementById('timed-send-panel').classList.add('active');
|
|
3165
|
-
updateTimedSendPreview();
|
|
3166
|
-
renderTimedTags();
|
|
3167
|
-
syncComposerButtonState();
|
|
3168
|
-
updateTerminalControlsHeight();
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
async function saveTimedSend() {
|
|
3172
|
-
if (!activeSessionId) return alert('No active session');
|
|
3173
|
-
const text = inputEl.value;
|
|
3174
|
-
if (!text.trim()) return alert('Type a message first');
|
|
3175
|
-
const sendAt = parseSendAt();
|
|
3176
|
-
if (!sendAt) return alert('Choose at least 1 minute');
|
|
3177
|
-
|
|
3178
|
-
try {
|
|
3179
|
-
const url = editingTimedInputId
|
|
3180
|
-
? `/api/sessions/${activeSessionId}/timed-inputs/${editingTimedInputId}`
|
|
3181
|
-
: `/api/sessions/${activeSessionId}/timed-inputs`;
|
|
3182
|
-
const res = await fetchWithTimeout(url, {
|
|
3183
|
-
method: editingTimedInputId ? 'PATCH' : 'POST',
|
|
3184
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3185
|
-
body: JSON.stringify({ text, sendAt })
|
|
3186
|
-
});
|
|
3187
|
-
const data = await res.json();
|
|
3188
|
-
if (!res.ok || !data.success) throw new Error(data.error || 'Failed to save timer');
|
|
3189
|
-
resetTimedEditor();
|
|
3190
|
-
await loadTimedInputs();
|
|
3191
|
-
} catch (e) {
|
|
3192
|
-
alert('Save failed: ' + e.message);
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
async function cancelTimedInput(id) {
|
|
3197
|
-
if (!activeSessionId) return;
|
|
3198
|
-
try {
|
|
3199
|
-
await fetchWithTimeout(`/api/sessions/${activeSessionId}/timed-inputs/${id}`, { method: 'DELETE' });
|
|
3200
|
-
if (editingTimedInputId === id) resetTimedEditor();
|
|
3201
|
-
loadTimedInputs();
|
|
3202
|
-
} catch (e) {
|
|
3203
|
-
alert('Delete failed');
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
function deleteEditingTimedInput() {
|
|
3208
|
-
if (editingTimedInputId) cancelTimedInput(editingTimedInputId);
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
document.getElementById('send-btn').addEventListener('click', performSend);
|
|
3212
|
-
document.getElementById('timer-btn').addEventListener('click', () => {
|
|
3213
|
-
const menu = document.getElementById('composer-menu');
|
|
3214
|
-
const willOpen = !menu.classList.contains('active');
|
|
3215
|
-
document.getElementById('timed-send-panel').classList.remove('active');
|
|
3216
|
-
menu.classList.toggle('active', willOpen);
|
|
3217
|
-
syncComposerButtonState();
|
|
3218
|
-
updateTerminalControlsHeight();
|
|
3219
|
-
});
|
|
3220
|
-
document.getElementById('attach-image-btn').addEventListener('click', () => {
|
|
3221
|
-
closeComposerMenu();
|
|
3222
|
-
imageFileInput.click();
|
|
3223
|
-
});
|
|
3224
|
-
document.getElementById('schedule-send-btn').addEventListener('click', openTimedSendPanel);
|
|
3225
|
-
imageFileInput.addEventListener('change', () => {
|
|
3226
|
-
const files = imageFileInput.files;
|
|
3227
|
-
if (files?.length) void uploadImageFiles(files);
|
|
3228
|
-
imageFileInput.value = '';
|
|
3229
|
-
});
|
|
3230
|
-
inputEl.addEventListener('keydown', (e) => {
|
|
3231
|
-
if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); performSend(); }
|
|
3232
|
-
else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
|
|
3233
|
-
markInputEditStart();
|
|
3234
|
-
if (keepTerminalBottomForNextInput) requestAnimationFrame(restoreTerminalBottomSoon);
|
|
3235
|
-
}
|
|
3236
|
-
});
|
|
3237
|
-
|
|
3238
|
-
document.getElementById('shortcut-rail').addEventListener('click', (e) => {
|
|
3239
|
-
const commandBtn = e.target.closest('.command-key');
|
|
3240
|
-
if (commandBtn) {
|
|
3241
|
-
const command = commandBtn.dataset.command;
|
|
3242
|
-
if (command) {
|
|
3243
|
-
sendWS(command);
|
|
3244
|
-
setTimeout(() => { sendWS('\r'); }, 1000);
|
|
3245
|
-
}
|
|
3246
|
-
return;
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
const keyBtn = e.target.closest('.key-btn'); if (!keyBtn) return;
|
|
3250
|
-
const key = keyBtn.dataset.key;
|
|
3251
|
-
if (key === 'ctrl') { modifiers[key] = !modifiers[key]; keyBtn.classList.toggle('active', modifiers[key]); return; }
|
|
3252
|
-
if (key === 'codex-perm') {
|
|
3253
|
-
sendWS('/perm');
|
|
3254
|
-
setTimeout(() => { sendWS('\r'); }, 1000);
|
|
3255
|
-
return;
|
|
3256
|
-
}
|
|
3257
|
-
const sequences = { up: '\x1b[A', down: '\x1b[B', left: '\x1b[D', right: '\x1b[C', enter: '\r', esc: '\x1b', tab: '\t', 'ctrl-c': '\x03', 'ctrl-y': '\x19' };
|
|
3258
|
-
if (sequences[key]) sendWS(sequences[key]);
|
|
3259
|
-
});
|
|
3260
|
-
|
|
3261
|
-
const scrollChargeDurationMs = 6000;
|
|
3262
|
-
let isScrolling = false, scrollDir = 0, scrollSpeed = 0, rafId = null;
|
|
3263
|
-
let chargeBtn = null, chargeStartTime = 0, chargeRafId = null, chargePointerId = null, chargeCompleted = false;
|
|
3264
|
-
|
|
3265
|
-
function getTerminalViewport() {
|
|
3266
|
-
return document.querySelector('.xterm-viewport');
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
function smoothScrollLoop() {
|
|
3270
|
-
if (!isScrolling) return;
|
|
3271
|
-
const viewport = getTerminalViewport();
|
|
3272
|
-
if (viewport) { viewport.scrollTop += (scrollDir * scrollSpeed); scrollSpeed = Math.min(30, scrollSpeed + 0.2); }
|
|
3273
|
-
rafId = requestAnimationFrame(smoothScrollLoop);
|
|
3274
|
-
}
|
|
3275
|
-
function startScroll(dir) { if (isScrolling) return; isScrolling = true; scrollDir = dir; scrollSpeed = 4; smoothScrollLoop(); }
|
|
3276
|
-
function stopScroll() { isScrolling = false; if (rafId) cancelAnimationFrame(rafId); }
|
|
3277
|
-
|
|
3278
|
-
function jumpTerminalScroll(dir) {
|
|
3279
|
-
const viewport = getTerminalViewport();
|
|
3280
|
-
if (!viewport) return;
|
|
3281
|
-
viewport.scrollTop = dir < 0 ? 0 : viewport.scrollHeight;
|
|
3282
|
-
if (term) {
|
|
3283
|
-
if (dir < 0) term.scrollToTop();
|
|
3284
|
-
else term.scrollToBottom();
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
|
|
3288
|
-
function resetChargeButton(btn) {
|
|
3289
|
-
if (!btn) return;
|
|
3290
|
-
btn.classList.remove('charging');
|
|
3291
|
-
btn.style.setProperty('--charge', '0deg');
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
function stopScrollCharge() {
|
|
3295
|
-
stopScroll();
|
|
3296
|
-
if (chargeRafId) cancelAnimationFrame(chargeRafId);
|
|
3297
|
-
if (chargeBtn && chargePointerId !== null) {
|
|
3298
|
-
try { chargeBtn.releasePointerCapture(chargePointerId); } catch (_) {}
|
|
3299
|
-
}
|
|
3300
|
-
resetChargeButton(chargeBtn);
|
|
3301
|
-
chargeBtn = null;
|
|
3302
|
-
chargeStartTime = 0;
|
|
3303
|
-
chargeRafId = null;
|
|
3304
|
-
chargePointerId = null;
|
|
3305
|
-
chargeCompleted = false;
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
function updateScrollCharge() {
|
|
3309
|
-
if (!chargeBtn) return;
|
|
3310
|
-
const elapsed = performance.now() - chargeStartTime;
|
|
3311
|
-
const progress = Math.min(1, elapsed / scrollChargeDurationMs);
|
|
3312
|
-
chargeBtn.style.setProperty('--charge', (progress * 360) + 'deg');
|
|
3313
|
-
if (progress >= 1) {
|
|
3314
|
-
chargeCompleted = true;
|
|
3315
|
-
jumpTerminalScroll(scrollDir);
|
|
3316
|
-
stopScrollCharge();
|
|
3317
|
-
return;
|
|
3318
|
-
}
|
|
3319
|
-
chargeRafId = requestAnimationFrame(updateScrollCharge);
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
function startScrollCharge(e, dir) {
|
|
3323
|
-
e.preventDefault();
|
|
3324
|
-
stopScrollCharge();
|
|
3325
|
-
chargeBtn = e.currentTarget;
|
|
3326
|
-
chargePointerId = e.pointerId;
|
|
3327
|
-
chargeStartTime = performance.now();
|
|
3328
|
-
chargeCompleted = false;
|
|
3329
|
-
chargeBtn.classList.add('charging');
|
|
3330
|
-
chargeBtn.setPointerCapture(chargePointerId);
|
|
3331
|
-
startScroll(dir);
|
|
3332
|
-
updateScrollCharge();
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
document.getElementById('scroll-up').addEventListener('pointerdown', (e) => startScrollCharge(e, -1));
|
|
3336
|
-
document.getElementById('scroll-down').addEventListener('pointerdown', (e) => startScrollCharge(e, 1));
|
|
3337
|
-
window.addEventListener('pointerup', stopScrollCharge);
|
|
3338
|
-
window.addEventListener('pointercancel', stopScrollCharge);
|
|
3339
|
-
window.addEventListener('resize', syncLayout);
|
|
3340
|
-
|
|
3341
|
-
// Git Logic
|
|
3342
|
-
let currentGitTab = 'changes';
|
|
3343
|
-
let currentDirPath = '';
|
|
3344
|
-
|
|
3345
|
-
function switchGitTab(tab) {
|
|
3346
|
-
currentGitTab = tab;
|
|
3347
|
-
document.getElementById('tab-changes').classList.toggle('active', tab === 'changes');
|
|
3348
|
-
document.getElementById('tab-directories').classList.toggle('active', tab === 'directories');
|
|
3349
|
-
document.getElementById('tab-graph').classList.toggle('active', tab === 'graph');
|
|
3350
|
-
if (tab === 'changes') {
|
|
3351
|
-
loadGitStatus();
|
|
3352
|
-
} else if (tab === 'directories') {
|
|
3353
|
-
loadDirectories(currentDirPath);
|
|
3354
|
-
} else if (tab === 'graph') {
|
|
3355
|
-
loadGitGraph();
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
let currentCommitDiffHTML = '';
|
|
3361
|
-
let currentCommitHash = '';
|
|
3362
|
-
|
|
3363
|
-
window.loadCommitDiff = async function(hash) {
|
|
3364
|
-
if (!activeSessionId) return;
|
|
3365
|
-
const content = document.getElementById('git-content');
|
|
3366
|
-
content.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-dim);">Loading diff...</div>';
|
|
3367
|
-
currentCommitHash = hash;
|
|
3368
|
-
|
|
3369
|
-
try {
|
|
3370
|
-
const res = await fetch(`/api/sessions/${activeSessionId}/git-show/${hash}`);
|
|
3371
|
-
const data = await res.json();
|
|
3372
|
-
if (data.success) {
|
|
3373
|
-
let fileBlocks = [];
|
|
3374
|
-
let currentBlock = { name: 'Commit Details', lines: [] };
|
|
3375
|
-
fileBlocks.push(currentBlock);
|
|
3376
|
-
|
|
3377
|
-
const lines = data.stdout.split('\n');
|
|
3378
|
-
for (const line of lines) {
|
|
3379
|
-
const diffMatch = line.match(/^diff --git a\/(.+?) b\//);
|
|
3380
|
-
if (diffMatch) {
|
|
3381
|
-
currentBlock = { name: diffMatch[1], lines: [] };
|
|
3382
|
-
fileBlocks.push(currentBlock);
|
|
3383
|
-
}
|
|
3384
|
-
currentBlock.lines.push(line);
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
let diffHTML = '';
|
|
3388
|
-
for (const block of fileBlocks) {
|
|
3389
|
-
if (block.lines.length === 0 || (block.lines.length === 1 && !block.lines[0])) continue;
|
|
3390
|
-
|
|
3391
|
-
let blockContent = '';
|
|
3392
|
-
let addCount = 0;
|
|
3393
|
-
let subCount = 0;
|
|
3394
|
-
for (const line of block.lines) {
|
|
3395
|
-
let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
|
|
3396
|
-
if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; addCount++; }
|
|
3397
|
-
else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; subCount++; }
|
|
3398
|
-
else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
|
|
3399
|
-
else if (line.startsWith('diff') || line.startsWith('index') || line.startsWith('commit') || line.startsWith('Author') || line.startsWith('Date')) color = '#fff';
|
|
3400
|
-
|
|
3401
|
-
blockContent += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding:2px 8px; white-space:pre-wrap; word-break:break-all;">${line.replace(/</g, '<').replace(/>/g, '>') || ' '}</div>`;
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
const isOpen = block.name === 'Commit Details';
|
|
3405
|
-
const statHTML = block.name !== 'Commit Details' ? `<span style="margin-left: 12px; font-family: monospace; font-size: 12px;"><span style="color:#4ade80;">+${addCount}</span> <span style="color:#f87171; margin-left:6px;">-${subCount}</span></span>` : '';
|
|
3406
|
-
diffHTML += `
|
|
3407
|
-
<details ${isOpen ? 'open' : ''} style="margin-bottom: 8px; border: 1px solid #333; border-radius: 4px; overflow: hidden;">
|
|
3408
|
-
<summary style="background: #1e1e1e; padding: 6px 10px; cursor: pointer; color: #fff; font-weight: 500; font-size: 13px; outline: none; user-select: none;">
|
|
3409
|
-
${block.name === 'Commit Details' ? '📝 ' : '📄 '}${block.name.replace(/</g, '<').replace(/>/g, '>')}${statHTML}
|
|
3410
|
-
</summary>
|
|
3411
|
-
<div style="background: #0d0d0d; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; padding: 4px 0;">
|
|
3412
|
-
${blockContent}
|
|
3413
|
-
</div>
|
|
3414
|
-
</details>
|
|
3415
|
-
`;
|
|
3416
|
-
}
|
|
3417
|
-
currentCommitDiffHTML = diffHTML;
|
|
3418
|
-
renderCommitDiffFullView();
|
|
3419
|
-
} else {
|
|
3420
|
-
content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Error loading diff</p>`;
|
|
3421
|
-
}
|
|
3422
|
-
} catch (e) {
|
|
3423
|
-
content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Network error</p>`;
|
|
3424
|
-
}
|
|
3425
|
-
};
|
|
3426
|
-
|
|
3427
|
-
function renderCommitDiffFullView() {
|
|
3428
|
-
const content = document.getElementById('git-content');
|
|
3429
|
-
let html = `
|
|
3430
|
-
<div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 10;">
|
|
3431
|
-
<button class="icon-btn" onclick="switchGitTab('graph')" style="color: var(--primary); margin-right: 12px; font-weight:600; font-size:14px; display:flex; align-items:center;">
|
|
3432
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> Back
|
|
3433
|
-
</button>
|
|
3434
|
-
<div style="flex:1; min-width:0;">
|
|
3435
|
-
<div style="font-weight:600; font-size:15px; font-family: monospace;">Commit: ${currentCommitHash}</div>
|
|
3436
|
-
</div>
|
|
3437
|
-
</div>
|
|
3438
|
-
<div style="padding: 10px;">
|
|
3439
|
-
${currentCommitDiffHTML}
|
|
3440
|
-
</div>`;
|
|
3441
|
-
content.innerHTML = html;
|
|
3442
|
-
}
|
|
3443
|
-
|
|
3444
|
-
async function loadGitGraph() {
|
|
3445
|
-
|
|
3446
|
-
if (!activeSessionId) return;
|
|
3447
|
-
const container = document.getElementById('git-content');
|
|
3448
|
-
container.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-dim);">Loading graph...</div>';
|
|
3449
|
-
try {
|
|
3450
|
-
const res = await fetch(`/api/sessions/${activeSessionId}/git-log`);
|
|
3451
|
-
const data = await res.json();
|
|
3452
|
-
if (data.success) {
|
|
3453
|
-
const renderer = new GitGraphRenderer(container);
|
|
3454
|
-
renderer.render(data.commits);
|
|
3455
|
-
} else {
|
|
3456
|
-
container.innerHTML = `<div style="padding: 20px; color: #ff3b30;">Error: ${data.error}</div>`;
|
|
3457
|
-
}
|
|
3458
|
-
} catch (e) {
|
|
3459
|
-
container.innerHTML = `<div style="padding: 20px; color: #ff3b30;">Failed to load graph</div>`;
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
async function loadDirectories(path = '') {
|
|
3464
|
-
currentDirPath = path;
|
|
3465
|
-
const content = document.getElementById('git-content');
|
|
3466
|
-
content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading...</p>';
|
|
3467
|
-
if (!activeSessionId) return;
|
|
3468
|
-
try {
|
|
3469
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/fs/dir?path=${encodeURIComponent(path)}`);
|
|
3470
|
-
const data = await res.json();
|
|
3471
|
-
if (!data.success) throw new Error(data.error);
|
|
3472
|
-
|
|
3473
|
-
let html = '<div>';
|
|
3474
|
-
if (path !== '') {
|
|
3475
|
-
const parts = path.split('/');
|
|
3476
|
-
parts.pop();
|
|
3477
|
-
const parentPath = parts.join('/');
|
|
3478
|
-
html += `<div class="list-item" onclick="loadDirectories(decodePathValue('${encodePathValue(parentPath)}'))">
|
|
3479
|
-
<svg class="dir-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
|
3480
|
-
<div style="font-size:14px; font-weight:500;">..</div>
|
|
3481
|
-
</div>`;
|
|
3482
|
-
}
|
|
3483
|
-
|
|
3484
|
-
if (data.files.length === 0) {
|
|
3485
|
-
html += '<p style="color:var(--text-dim); text-align:center; padding: 20px;">Empty directory</p>';
|
|
3486
|
-
} else {
|
|
3487
|
-
data.files.forEach(f => {
|
|
3488
|
-
const fullPath = path ? `${path}/${f.name}` : f.name;
|
|
3489
|
-
const encodedPath = encodePathValue(fullPath);
|
|
3490
|
-
const escapedName = escapeHtml(f.name);
|
|
3491
|
-
let colorStyle = '';
|
|
3492
|
-
let badgeHtml = '';
|
|
3493
|
-
if (f.gitStatus) {
|
|
3494
|
-
let color = '#fff';
|
|
3495
|
-
let label = f.gitStatus.trim();
|
|
3496
|
-
if (label.includes('U') || label.includes('?')) { colorStyle = 'color: #4ade80;'; color = '#4ade80'; if(label === '??') label = 'U'; }
|
|
3497
|
-
else if (label.includes('M')) { colorStyle = 'color: #f59e0b;'; color = '#f59e0b'; }
|
|
3498
|
-
else if (label.includes('D')) { colorStyle = 'color: #f87171;'; color = '#f87171'; }
|
|
3499
|
-
badgeHtml = `<span style="color:${color}; font-weight:700; font-size:10px; border:1px solid ${color}; padding:1px 4px; border-radius:3px; opacity:0.7; margin-left: auto;">${label}</span>`;
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
if (f.isDirectory) {
|
|
3503
|
-
html += `<div class="list-item" onclick="loadDirectories(decodePathValue('${encodedPath}'))">
|
|
3504
|
-
<svg class="dir-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>
|
|
3505
|
-
<div style="font-size:14px; font-weight:500; ${colorStyle}">${escapedName}</div>
|
|
3506
|
-
${badgeHtml}
|
|
3507
|
-
</div>`;
|
|
3508
|
-
} else {
|
|
3509
|
-
html += `<div class="list-item" onclick="showFileDetails(decodePathValue('${encodedPath}'), false)">
|
|
3510
|
-
<svg class="file-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
|
|
3511
|
-
<div style="font-size:14px; font-weight:500; ${colorStyle}">${escapedName}</div>
|
|
3512
|
-
${badgeHtml}
|
|
3513
|
-
</div>`;
|
|
3514
|
-
}
|
|
3515
|
-
});
|
|
3516
|
-
}
|
|
3517
|
-
html += '</div>';
|
|
3518
|
-
content.innerHTML = html;
|
|
3519
|
-
} catch (e) {
|
|
3520
|
-
content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`;
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
async function loadGitStatus() {
|
|
3525
|
-
const content = document.getElementById('git-content');
|
|
3526
|
-
content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading...</p>';
|
|
3527
|
-
if (!activeSessionId) return;
|
|
3528
|
-
try {
|
|
3529
|
-
const [resStatus, resUnstaged, resStaged] = await Promise.all([
|
|
3530
|
-
fetchWithTimeout(`/api/sessions/${activeSessionId}/git-status`),
|
|
3531
|
-
fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-numstat?staged=false`).catch(() => ({ok:false})),
|
|
3532
|
-
fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-numstat?staged=true`).catch(() => ({ok:false}))
|
|
3533
|
-
]);
|
|
3534
|
-
|
|
3535
|
-
const data = await resStatus.json();
|
|
3536
|
-
if (!data.success) {
|
|
3537
|
-
content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Git error: ${data.error}</p>`;
|
|
3538
|
-
return;
|
|
3539
|
-
}
|
|
3540
|
-
const files = Array.isArray(data.files) ? data.files : [];
|
|
3541
|
-
|
|
3542
|
-
if (files.length === 0) {
|
|
3543
|
-
content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">No changes detected.</p>';
|
|
3544
|
-
return;
|
|
3545
|
-
}
|
|
3546
|
-
|
|
3547
|
-
let statsMap = {};
|
|
3548
|
-
const parseNumstat = (output) => {
|
|
3549
|
-
if (!output) return;
|
|
3550
|
-
output.split('\n').forEach(line => {
|
|
3551
|
-
const parts = line.split('\t');
|
|
3552
|
-
if (parts.length >= 3) {
|
|
3553
|
-
const added = parseInt(parts[0]) || 0;
|
|
3554
|
-
const removed = parseInt(parts[1]) || 0;
|
|
3555
|
-
const file = parts.slice(2).join('\t');
|
|
3556
|
-
if (!statsMap[file]) statsMap[file] = { added: 0, removed: 0 };
|
|
3557
|
-
statsMap[file].added += added;
|
|
3558
|
-
statsMap[file].removed += removed;
|
|
3559
|
-
}
|
|
3560
|
-
});
|
|
3561
|
-
};
|
|
3562
|
-
|
|
3563
|
-
if (resUnstaged.ok) {
|
|
3564
|
-
const unstagedData = await resUnstaged.json();
|
|
3565
|
-
if (unstagedData.success) parseNumstat(unstagedData.stdout);
|
|
3566
|
-
}
|
|
3567
|
-
if (resStaged.ok) {
|
|
3568
|
-
const stagedData = await resStaged.json();
|
|
3569
|
-
if (stagedData.success) parseNumstat(stagedData.stdout);
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
let html = '<div style="padding:10px;">';
|
|
3573
|
-
files.forEach((f, idx) => {
|
|
3574
|
-
const encodedPath = encodePathValue(f.path);
|
|
3575
|
-
const escapedPath = escapeHtml(f.path);
|
|
3576
|
-
let color = '#fff';
|
|
3577
|
-
let label = f.status;
|
|
3578
|
-
if (label.includes('M')) { color = '#f59e0b'; }
|
|
3579
|
-
else if (label.includes('A') || label === '??') { color = '#4ade80'; if(label === '??') label = 'U'; }
|
|
3580
|
-
else if (label.includes('D')) { color = '#f87171'; }
|
|
3581
|
-
const isUntracked = f.status === '??';
|
|
3582
|
-
const hasStaged = !isUntracked && f.status[0] && f.status[0] !== ' ';
|
|
3583
|
-
const hasUnstaged = !isUntracked && f.status[1] && f.status[1] !== ' ';
|
|
3584
|
-
|
|
3585
|
-
let statHTML = '';
|
|
3586
|
-
if (statsMap[f.path]) {
|
|
3587
|
-
const { added, removed } = statsMap[f.path];
|
|
3588
|
-
if (added > 0 || removed > 0) {
|
|
3589
|
-
statHTML = `<span style="margin-left: 12px; font-family: monospace; font-size: 12px; white-space: nowrap;"><span style="color:#4ade80;">+${added}</span> <span style="color:#f87171; margin-left:6px;">-${removed}</span></span>`;
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
html += `<div style="margin-bottom: 8px; border: 1px solid #333; border-radius: 4px; background: #1e1e1e; overflow: hidden;">
|
|
3594
|
-
<div onclick="toggleInlineDiff('${encodedPath}', 'inline-diff-${idx}', ${!!hasStaged}, ${!!hasUnstaged}, ${isUntracked})" style="padding: 6px 10px; cursor: pointer; color: #fff; font-weight: 500; font-size: 13px; outline: none; user-select: none; display: flex; align-items: center;">
|
|
3595
|
-
<div style="flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">📄 ${escapedPath}${statHTML}</div>
|
|
3596
|
-
<span style="color:${color}; font-weight:700; font-size:10px; border:1px solid ${color}; padding:1px 4px; border-radius:3px; opacity:0.8; flex-shrink: 0; margin-left: 8px;">${label}</span>
|
|
3597
|
-
</div>
|
|
3598
|
-
<div id="inline-diff-${idx}" style="display: none;" data-loaded="false"></div>
|
|
3599
|
-
</div>`;
|
|
3600
|
-
});
|
|
3601
|
-
html += '</div>';
|
|
3602
|
-
content.innerHTML = html;
|
|
3603
|
-
} catch (e) {
|
|
3604
|
-
content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`;
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
|
-
function buildFileDiffUrl(path, staged) {
|
|
3609
|
-
return `/api/sessions/${activeSessionId}/git-diff-file?path=${encodeURIComponent(path)}&staged=${staged ? 'true' : 'false'}`;
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
async function loadFileChangeData(path, options = {}) {
|
|
3613
|
-
const hasStaged = !!options.hasStaged;
|
|
3614
|
-
const hasUnstaged = !!options.hasUnstaged;
|
|
3615
|
-
const isUntracked = !!options.isUntracked;
|
|
3616
|
-
const diffRequests = [];
|
|
3617
|
-
|
|
3618
|
-
if (hasStaged) {
|
|
3619
|
-
diffRequests.push({
|
|
3620
|
-
label: 'Staged changes',
|
|
3621
|
-
promise: fetchWithTimeout(buildFileDiffUrl(path, true)).catch(() => ({ok:false}))
|
|
3622
|
-
});
|
|
3623
|
-
}
|
|
3624
|
-
if (hasUnstaged || (!hasStaged && !isUntracked)) {
|
|
3625
|
-
diffRequests.push({
|
|
3626
|
-
label: 'Unstaged changes',
|
|
3627
|
-
promise: fetchWithTimeout(buildFileDiffUrl(path, false)).catch(() => ({ok:false}))
|
|
3628
|
-
});
|
|
3629
|
-
}
|
|
3630
|
-
|
|
3631
|
-
const [diffResponses, fileRes] = await Promise.all([
|
|
3632
|
-
Promise.all(diffRequests.map(item => item.promise)),
|
|
3633
|
-
fetchWithTimeout(`/api/sessions/${activeSessionId}/file?path=${encodeURIComponent(path)}`).catch(() => ({ok:false}))
|
|
3634
|
-
]);
|
|
3635
|
-
|
|
3636
|
-
const diffParts = [];
|
|
3637
|
-
for (let i = 0; i < diffResponses.length; i++) {
|
|
3638
|
-
const res = diffResponses[i];
|
|
3639
|
-
const data = res.ok ? await res.json() : { success: false };
|
|
3640
|
-
if (data.success && data.stdout) {
|
|
3641
|
-
diffParts.push({ label: diffRequests[i].label, stdout: data.stdout });
|
|
3642
|
-
}
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
const fileData = fileRes.ok ? await fileRes.json() : { success: false };
|
|
3646
|
-
const content = fileData.success ? fileData.content : '';
|
|
3647
|
-
let diff = diffParts.map(part => (
|
|
3648
|
-
diffParts.length > 1 ? `# ${part.label}\n${part.stdout}` : part.stdout
|
|
3649
|
-
)).join('\n');
|
|
3650
|
-
if (!diff && isUntracked && content) diff = 'Untracked file:\n\n' + content;
|
|
3651
|
-
|
|
3652
|
-
return {
|
|
3653
|
-
diff,
|
|
3654
|
-
content,
|
|
3655
|
-
renderRawDiff: diffParts.length > 1
|
|
3656
|
-
};
|
|
3657
|
-
}
|
|
3658
|
-
|
|
3659
|
-
window.toggleInlineDiff = async function(encodedPath, containerId, hasStaged = false, hasUnstaged = true, isUntracked = false) {
|
|
3660
|
-
const container = document.getElementById(containerId);
|
|
3661
|
-
if (container.style.display === 'block') {
|
|
3662
|
-
container.style.display = 'none';
|
|
3663
|
-
return;
|
|
3664
|
-
}
|
|
3665
|
-
container.style.display = 'block';
|
|
3666
|
-
if (container.dataset.loaded === 'true') return;
|
|
3667
|
-
|
|
3668
|
-
const path = decodePathValue(encodedPath);
|
|
3669
|
-
container.innerHTML = '<div style="padding: 10px; color: var(--text-dim); text-align: center; font-size: 12px;">Loading...</div>';
|
|
3670
|
-
|
|
3671
|
-
try {
|
|
3672
|
-
const { diff: currentFileDiff, content: currentFileContent } = await loadFileChangeData(path, {
|
|
3673
|
-
hasStaged,
|
|
3674
|
-
hasUnstaged,
|
|
3675
|
-
isUntracked
|
|
3676
|
-
});
|
|
3677
|
-
|
|
3678
|
-
if (!currentFileDiff && !currentFileContent) {
|
|
3679
|
-
container.innerHTML = `<div style="padding: 10px; color: #f87171; font-size: 12px; text-align:center;">No diff available</div>`;
|
|
3680
|
-
return;
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
let blockContent = '';
|
|
3684
|
-
currentFileDiff.split('\n').forEach(line => {
|
|
3685
|
-
let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
|
|
3686
|
-
if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
|
|
3687
|
-
else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
|
|
3688
|
-
else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
|
|
3689
|
-
|
|
3690
|
-
blockContent += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding:2px 8px; white-space:pre-wrap; word-break:break-all;">${escapeHtml(line) || ' '}</div>`;
|
|
3691
|
-
});
|
|
3692
|
-
|
|
3693
|
-
container.dataset.loaded = 'true';
|
|
3694
|
-
container.innerHTML = `
|
|
3695
|
-
<div style="background: #0d0d0d; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; padding: 4px 0; border-top: 1px solid #333; max-height: 400px;">
|
|
3696
|
-
${blockContent}
|
|
3697
|
-
</div>
|
|
3698
|
-
<div style="padding: 8px; background: #1a1a1a; border-top: 1px solid #333; text-align: center;">
|
|
3699
|
-
<button onclick="showFileDetails(decodePathValue('${encodedPath}'), true, ${!!hasStaged}, ${!!hasUnstaged}, ${!!isUntracked})" style="background: var(--primary); border: none; color: #fff; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;">
|
|
3700
|
-
<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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
|
|
3701
|
-
View Full File
|
|
3702
|
-
</button>
|
|
3703
|
-
</div>
|
|
3704
|
-
`;
|
|
3705
|
-
} catch (e) {
|
|
3706
|
-
container.innerHTML = `<div style="padding: 10px; color: #f87171; font-size: 12px; text-align:center;">Error: ${e.message}</div>`;
|
|
3707
|
-
}
|
|
3708
|
-
};
|
|
3709
|
-
|
|
3710
|
-
let currentFilePath = '', currentFileDiff = '', currentFileContent = '', currentFileMode = 'diff', currentFileWrap = false, currentFontSize = 12, currentFileRenderRawDiff = false;
|
|
3711
|
-
|
|
3712
|
-
function changeFontSize(delta) {
|
|
3713
|
-
if (delta === 0) currentFontSize = 12;
|
|
3714
|
-
else currentFontSize = Math.max(8, Math.min(32, currentFontSize + delta));
|
|
3715
|
-
renderFileDetails();
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
async function showFileDetails(path, isFromChanges = true, hasStaged = false, hasUnstaged = true, isUntracked = false) {
|
|
3719
|
-
currentFilePath = path;
|
|
3720
|
-
currentFileMode = isFromChanges ? 'diff' : 'file';
|
|
3721
|
-
currentFileRenderRawDiff = false;
|
|
3722
|
-
const content = document.getElementById('git-content');
|
|
3723
|
-
content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading details...</p>';
|
|
3724
|
-
try {
|
|
3725
|
-
const detailData = await loadFileChangeData(path, {
|
|
3726
|
-
hasStaged: isFromChanges ? hasStaged : false,
|
|
3727
|
-
hasUnstaged: isFromChanges ? hasUnstaged : true,
|
|
3728
|
-
isUntracked: isFromChanges ? isUntracked : false
|
|
3729
|
-
});
|
|
3730
|
-
currentFileDiff = detailData.diff;
|
|
3731
|
-
currentFileContent = detailData.content;
|
|
3732
|
-
currentFileRenderRawDiff = detailData.renderRawDiff;
|
|
3733
|
-
if (!currentFileDiff && !currentFileContent) {
|
|
3734
|
-
content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Failed to load details.</p>`;
|
|
3735
|
-
return;
|
|
3736
|
-
}
|
|
3737
|
-
if (!currentFileDiff && !isFromChanges) currentFileMode = 'file';
|
|
3738
|
-
renderFileDetails();
|
|
3739
|
-
} catch (e) { content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`; }
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
|
-
function toggleFileMode() { currentFileMode = currentFileMode === 'diff' ? 'file' : 'diff'; renderFileDetails(); }
|
|
3743
|
-
function toggleFileWrap() { currentFileWrap = !currentFileWrap; renderFileDetails(); }
|
|
3744
|
-
|
|
3745
|
-
function renderFileDetails() {
|
|
3746
|
-
const content = document.getElementById('git-content');
|
|
3747
|
-
let html = `
|
|
3748
|
-
<div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 10;">
|
|
3749
|
-
<button class="icon-btn" onclick="switchGitTab(currentGitTab)" style="color: var(--primary); margin-right: 12px; font-weight:600; font-size:14px; display:flex; align-items:center; flex-shrink: 0;">
|
|
3750
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> Back
|
|
3751
|
-
</button>
|
|
3752
|
-
<div style="flex:1; min-width:0; margin-right: 12px;">
|
|
3753
|
-
<div style="font-weight:600; font-size:15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(currentFilePath)}">${escapeHtml(currentFilePath.split('/').pop() || currentFilePath)}</div>
|
|
3754
|
-
</div>`;
|
|
3755
|
-
|
|
3756
|
-
if (currentFileDiff && currentFileContent) {
|
|
3757
|
-
html += `
|
|
3758
|
-
<div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
|
|
3759
|
-
<div style="display: flex; gap: 2px; margin-right: 8px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px;">
|
|
3760
|
-
<button onclick="changeFontSize(-1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom Out" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A-</button>
|
|
3761
|
-
<button onclick="changeFontSize(0)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Reset Size" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">${currentFontSize}</button>
|
|
3762
|
-
<button onclick="changeFontSize(1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom In" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A+</button>
|
|
3763
|
-
</div>
|
|
3764
|
-
<button onclick="toggleFileMode()" style="background: ${currentFileMode === 'diff' ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
|
|
3765
|
-
Diff
|
|
3766
|
-
</button>
|
|
3767
|
-
<button onclick="toggleFileWrap()" style="background: ${currentFileWrap ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
|
|
3768
|
-
Wrap
|
|
3769
|
-
</button>
|
|
3770
|
-
</div>`;
|
|
3771
|
-
} else {
|
|
3772
|
-
html += `
|
|
3773
|
-
<div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
|
|
3774
|
-
<div style="display: flex; gap: 2px; margin-right: 8px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px;">
|
|
3775
|
-
<button onclick="changeFontSize(-1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom Out" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A-</button>
|
|
3776
|
-
<button onclick="changeFontSize(0)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Reset Size" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">${currentFontSize}</button>
|
|
3777
|
-
<button onclick="changeFontSize(1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom In" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A+</button>
|
|
3778
|
-
</div>
|
|
3779
|
-
<button onclick="toggleFileWrap()" style="background: ${currentFileWrap ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
|
|
3780
|
-
Wrap
|
|
3781
|
-
</button>
|
|
3782
|
-
</div>`;
|
|
3783
|
-
}
|
|
3784
|
-
|
|
3785
|
-
html += `</div>`;
|
|
3786
|
-
|
|
3787
|
-
const wrapStyle = currentFileWrap ? 'white-space: pre-wrap; word-break: break-all;' : 'white-space: pre;';
|
|
3788
|
-
const innerWrapStyle = currentFileWrap ? '' : 'min-width: max-content;';
|
|
3789
|
-
html += `<div style="padding: 10px;">`;
|
|
3790
|
-
html += `<div style="background:#0d0d0d; border-radius:8px; overflow-x:auto; font-family:monospace; font-size:${currentFontSize}px; line-height:1.5; padding: 12px 0; margin: 0; -webkit-text-size-adjust: 100%; text-size-adjust: 100%;">`;
|
|
3791
|
-
html += `<div style="${innerWrapStyle}">`;
|
|
3792
|
-
|
|
3793
|
-
let mergedLines = [];
|
|
3794
|
-
const lines = currentFileContent ? currentFileContent.replace(/\r\n/g, '\n').split('\n') : [];
|
|
3795
|
-
|
|
3796
|
-
if (currentFileMode === 'diff' && currentFileDiff && !currentFileDiff.startsWith('Untracked file:')) {
|
|
3797
|
-
let diffHunks = [];
|
|
3798
|
-
let currentHunk = null;
|
|
3799
|
-
const diffLines = currentFileDiff.replace(/\r\n/g, '\n').split('\n');
|
|
3800
|
-
for (const dl of diffLines) {
|
|
3801
|
-
if (dl.startsWith('---') || dl.startsWith('+++')) continue;
|
|
3802
|
-
let match = dl.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
3803
|
-
if (match) {
|
|
3804
|
-
currentHunk = {
|
|
3805
|
-
newStart: parseInt(match[2], 10),
|
|
3806
|
-
lines: []
|
|
3807
|
-
};
|
|
3808
|
-
diffHunks.push(currentHunk);
|
|
3809
|
-
} else if (currentHunk) {
|
|
3810
|
-
currentHunk.lines.push(dl);
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
|
-
if (currentFileRenderRawDiff || diffHunks.length === 0) {
|
|
3815
|
-
currentFileDiff.replace(/\r\n/g, '\n').split('\n').forEach(line => {
|
|
3816
|
-
mergedLines.push({ type: 'raw', text: line });
|
|
3817
|
-
});
|
|
3818
|
-
} else {
|
|
3819
|
-
let contentLineIdx = 1;
|
|
3820
|
-
for (let hunk of diffHunks) {
|
|
3821
|
-
while (contentLineIdx < hunk.newStart && contentLineIdx <= lines.length) {
|
|
3822
|
-
mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
|
|
3823
|
-
contentLineIdx++;
|
|
3824
|
-
}
|
|
3825
|
-
for (let dl of hunk.lines) {
|
|
3826
|
-
if (dl.startsWith('-')) {
|
|
3827
|
-
mergedLines.push({ type: 'deleted', text: dl.substring(1) });
|
|
3828
|
-
} else if (dl.startsWith('+')) {
|
|
3829
|
-
mergedLines.push({ type: 'added', text: dl.substring(1), lineNum: contentLineIdx });
|
|
3830
|
-
contentLineIdx++;
|
|
3831
|
-
} else if (dl.startsWith(' ')) {
|
|
3832
|
-
mergedLines.push({ type: 'normal', text: dl.substring(1), lineNum: contentLineIdx });
|
|
3833
|
-
contentLineIdx++;
|
|
3834
|
-
}
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
while (contentLineIdx <= lines.length) {
|
|
3838
|
-
mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
|
|
3839
|
-
contentLineIdx++;
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
} else {
|
|
3843
|
-
lines.forEach((line, idx) => {
|
|
3844
|
-
mergedLines.push({ type: 'normal', text: line, lineNum: idx + 1 });
|
|
3845
|
-
});
|
|
3846
|
-
}
|
|
3847
|
-
|
|
3848
|
-
mergedLines.forEach(item => {
|
|
3849
|
-
let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
|
|
3850
|
-
let prefix = ' ';
|
|
3851
|
-
if (item.type === 'added') { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; prefix = '+'; }
|
|
3852
|
-
else if (item.type === 'deleted') { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; prefix = '-'; }
|
|
3853
|
-
else if (item.type === 'raw') {
|
|
3854
|
-
if (item.text.startsWith('+') && !item.text.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
|
|
3855
|
-
else if (item.text.startsWith('-') && !item.text.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
|
|
3856
|
-
else if (item.text.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
|
|
3857
|
-
prefix = '';
|
|
3858
|
-
}
|
|
3859
|
-
|
|
3860
|
-
const renderedText = item.type === 'raw' ? item.text : `${prefix} ${item.text}`;
|
|
3861
|
-
html += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding: 0 12px 0 8px; display:flex;">`;
|
|
3862
|
-
html += `<div style="color:#555; margin-right:16px; user-select:none; flex-shrink:0; width: 36px; text-align:right;">${item.lineNum || ''}</div>`;
|
|
3863
|
-
html += `<div style="flex:1; min-width:0; ${wrapStyle}">${escapeHtml(renderedText) || ' '}</div>`;
|
|
3864
|
-
html += `</div>`;
|
|
3865
|
-
});
|
|
3866
|
-
|
|
3867
|
-
html += `</div></div></div>`;
|
|
3868
|
-
content.innerHTML = html;
|
|
3869
|
-
}
|
|
3870
|
-
|
|
3871
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
3872
|
-
refreshSessionsNow();
|
|
3873
|
-
document.addEventListener('visibilitychange', refreshSessionsNow);
|
|
3874
|
-
});
|
|
3875
|
-
</script>
|
|
301
|
+
<script src="core.js"></script>
|
|
302
|
+
<script src="claude.js"></script>
|
|
303
|
+
<script src="schedules.js"></script>
|
|
304
|
+
<script src="shell.js"></script>
|
|
305
|
+
<script src="codex.js"></script>
|
|
306
|
+
<script src="session.js"></script>
|
|
307
|
+
<script src="composer.js"></script>
|
|
308
|
+
<script src="timed-inputs.js"></script>
|
|
309
|
+
<script src="terminal-scroll.js"></script>
|
|
310
|
+
<script src="git.js"></script>
|
|
3876
311
|
</body>
|
|
3877
312
|
</html>
|