@rotriz/pi-web-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/extension.mjs +107 -0
- package/index.html +2622 -0
- package/package.json +35 -0
- package/server.mjs +1398 -0
package/index.html
ADDED
|
@@ -0,0 +1,2622 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="theme-color" content="#f5f8ff" />
|
|
7
|
+
<script>
|
|
8
|
+
/* Resolve theme before first paint to avoid a light flash on dark systems. */
|
|
9
|
+
(function () {
|
|
10
|
+
try {
|
|
11
|
+
var pref = localStorage.getItem("pi-theme");
|
|
12
|
+
var mode = pref === "light" || pref === "dark" ? pref
|
|
13
|
+
: matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
14
|
+
document.documentElement.dataset.theme = mode;
|
|
15
|
+
} catch (_) { document.documentElement.dataset.theme = "light"; }
|
|
16
|
+
})();
|
|
17
|
+
</script>
|
|
18
|
+
<title>Pi Agent</title>
|
|
19
|
+
<style>
|
|
20
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
21
|
+
* Design system tokens — Bento
|
|
22
|
+
* Modular grid interface: friendly tiles, soft borders, balanced product
|
|
23
|
+
* density. Blue accent (#2563eb) reserved for primary actions; neutrals do
|
|
24
|
+
* the structural work. Tokens below are the brand contract — no raw hex
|
|
25
|
+
* outside this block; dark values are derived via color-mix only.
|
|
26
|
+
* ─────────────────────────────────────────────────────────────────── */
|
|
27
|
+
:root {
|
|
28
|
+
--bg: #f5f8ff;
|
|
29
|
+
--surface: #ffffff;
|
|
30
|
+
--surface-warm: #eaf1ff;
|
|
31
|
+
--fg: #101828;
|
|
32
|
+
--fg-2: #344054;
|
|
33
|
+
--muted: #667085;
|
|
34
|
+
--meta: #2563eb;
|
|
35
|
+
--border: #d7e0ef;
|
|
36
|
+
--border-soft: #edf2f8;
|
|
37
|
+
--accent: #2563eb;
|
|
38
|
+
--accent-on: #ffffff;
|
|
39
|
+
--accent-hover: color-mix(in oklab, var(--accent), black 8%);
|
|
40
|
+
--accent-active: color-mix(in oklab, var(--accent), black 14%);
|
|
41
|
+
--success: #16a34a;
|
|
42
|
+
--warn: #f59e0b;
|
|
43
|
+
--danger: #ef4444;
|
|
44
|
+
--font-display: Inter, system-ui, sans-serif;
|
|
45
|
+
--font-body: Inter, system-ui, sans-serif;
|
|
46
|
+
--font-mono: "SF Mono", ui-monospace, Menlo, monospace;
|
|
47
|
+
--text-xs: 12px;
|
|
48
|
+
--text-sm: 14px;
|
|
49
|
+
--text-base: 16px;
|
|
50
|
+
--text-lg: 18px;
|
|
51
|
+
--text-xl: 24px;
|
|
52
|
+
--text-2xl: 36px;
|
|
53
|
+
--leading-body: 1.52;
|
|
54
|
+
--leading-tight: 1.06;
|
|
55
|
+
--tracking-display: -0.025em;
|
|
56
|
+
--space-1: 4px;
|
|
57
|
+
--space-2: 8px;
|
|
58
|
+
--space-3: 12px;
|
|
59
|
+
--space-4: 16px;
|
|
60
|
+
--space-5: 20px;
|
|
61
|
+
--space-6: 24px;
|
|
62
|
+
--space-8: 32px;
|
|
63
|
+
--radius-sm: 10px;
|
|
64
|
+
--radius-md: 16px;
|
|
65
|
+
--radius-lg: 24px;
|
|
66
|
+
--radius-pill: 9999px;
|
|
67
|
+
--elev-flat: none;
|
|
68
|
+
--elev-ring: 0 0 0 1px var(--border);
|
|
69
|
+
--elev-raised: 0 20px 52px rgba(16, 24, 40, 0.11);
|
|
70
|
+
--focus-ring: 0 0 0 4px rgba(37, 99, 235, 0.22);
|
|
71
|
+
--motion-fast: 150ms;
|
|
72
|
+
--motion-base: 240ms;
|
|
73
|
+
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
74
|
+
|
|
75
|
+
/* Interaction tints — ink overlays in light mode, paper overlays in
|
|
76
|
+
dark mode (see the [data-theme="dark"] block below). */
|
|
77
|
+
--tint-subtle: rgba(16, 24, 40, 0.03);
|
|
78
|
+
--tint-hover: rgba(16, 24, 40, 0.05);
|
|
79
|
+
--scroll-thumb: rgba(16, 24, 40, 0.18);
|
|
80
|
+
--scroll-thumb-hover: rgba(16, 24, 40, 0.3);
|
|
81
|
+
|
|
82
|
+
/* Structural layout tokens (non-color) */
|
|
83
|
+
--sidebar-width: 264px;
|
|
84
|
+
--sidebar-collapsed: 60px;
|
|
85
|
+
--details-width: 400px;
|
|
86
|
+
--chat-max: 780px;
|
|
87
|
+
|
|
88
|
+
color-scheme: light;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ─── Dark theme — deep navy derived from Bento's own values: the light
|
|
92
|
+
* canvas inverts to a Deep Navy ground built on the fg token (#101828)
|
|
93
|
+
* mixed toward black; containers lift through fg-2 (#344054); text ramps
|
|
94
|
+
* down from the bg token (#f5f8ff). Accent stays Bento blue — it carries
|
|
95
|
+
* contrast on dark surfaces without shifting hue.
|
|
96
|
+
* ───────────────────────────────────────────────────────────── */
|
|
97
|
+
html[data-theme="dark"] {
|
|
98
|
+
--bg: color-mix(in oklab, #101828 80%, black);
|
|
99
|
+
--surface: color-mix(in oklab, #101828 62%, #344054);
|
|
100
|
+
--surface-warm: color-mix(in oklab, #101828 42%, #344054);
|
|
101
|
+
|
|
102
|
+
--fg: #f5f8ff;
|
|
103
|
+
--fg-2: color-mix(in oklab, #f5f8ff 72%, #667085);
|
|
104
|
+
--muted: color-mix(in oklab, #667085 55%, #f5f8ff);
|
|
105
|
+
--meta: color-mix(in oklab, #2563eb 55%, #f5f8ff);
|
|
106
|
+
|
|
107
|
+
--border: color-mix(in oklab, #101828 50%, #344054);
|
|
108
|
+
--border-soft: color-mix(in oklab, #101828 32%, #344054);
|
|
109
|
+
|
|
110
|
+
--accent-on: #ffffff;
|
|
111
|
+
|
|
112
|
+
--success: color-mix(in oklab, #16a34a 80%, white 10%);
|
|
113
|
+
--warn: color-mix(in oklab, #f59e0b 85%, white 10%);
|
|
114
|
+
--danger: color-mix(in oklab, #ef4444 82%, white 12%);
|
|
115
|
+
|
|
116
|
+
--elev-raised: 0 20px 52px rgba(0, 0, 0, 0.45);
|
|
117
|
+
|
|
118
|
+
--tint-subtle: rgba(245, 248, 255, 0.04);
|
|
119
|
+
--tint-hover: rgba(245, 248, 255, 0.07);
|
|
120
|
+
--scroll-thumb: rgba(245, 248, 255, 0.2);
|
|
121
|
+
--scroll-thumb-hover: rgba(245, 248, 255, 0.32);
|
|
122
|
+
|
|
123
|
+
color-scheme: dark;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
* { box-sizing: border-box; }
|
|
127
|
+
html { height: 100%; background: var(--bg); }
|
|
128
|
+
body {
|
|
129
|
+
min-height: 100%; height: 100%; margin: 0; overflow: hidden;
|
|
130
|
+
color: var(--fg); background: var(--bg);
|
|
131
|
+
font: var(--text-sm)/var(--leading-body) var(--font-body);
|
|
132
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
133
|
+
}
|
|
134
|
+
button, textarea, select, input { font: inherit; }
|
|
135
|
+
button { color: inherit; }
|
|
136
|
+
code, pre, .mono { font-family: var(--font-mono); }
|
|
137
|
+
::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); color: var(--fg); }
|
|
138
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
139
|
+
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 4px; }
|
|
140
|
+
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); }
|
|
141
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
142
|
+
|
|
143
|
+
.icon { width: 16px; height: 16px; display: block; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
144
|
+
.muted { color: var(--muted); }
|
|
145
|
+
|
|
146
|
+
/* Theme segmented control (Settings → General) */
|
|
147
|
+
.seg {
|
|
148
|
+
display: inline-flex; gap: 4px; padding: 4px; margin-bottom: 8px;
|
|
149
|
+
background: var(--bg); border-radius: var(--radius-md); box-shadow: var(--elev-ring);
|
|
150
|
+
}
|
|
151
|
+
.seg-btn {
|
|
152
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer;
|
|
153
|
+
padding: 6px 16px; border-radius: var(--radius-sm); font-size: var(--text-sm);
|
|
154
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
155
|
+
}
|
|
156
|
+
.seg-btn:hover { color: var(--fg); }
|
|
157
|
+
.seg-btn.active {
|
|
158
|
+
background: var(--surface-warm); color: var(--fg); font-weight: 550;
|
|
159
|
+
box-shadow: 0 0 0 1px var(--border-soft);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
button:focus-visible, [tabindex]:focus-visible, .sess:focus-visible {
|
|
163
|
+
outline: none; box-shadow: var(--focus-ring);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#app { display: flex; width: 100%; height: 100dvh; min-width: 0; overflow: hidden; }
|
|
167
|
+
#mobile-mask { display: none; }
|
|
168
|
+
|
|
169
|
+
/* ─── Sidebar ─────────────────────────────────────────────────────── */
|
|
170
|
+
#sidebar {
|
|
171
|
+
position: relative; z-index: 30; width: var(--sidebar-width); flex: 0 0 var(--sidebar-width);
|
|
172
|
+
display: flex; flex-direction: column; min-height: 0; overflow: hidden;
|
|
173
|
+
background: var(--surface);
|
|
174
|
+
border-right: 1px solid var(--border-soft);
|
|
175
|
+
transition: width .24s var(--ease-standard), flex-basis .24s var(--ease-standard);
|
|
176
|
+
}
|
|
177
|
+
body.sidebar-collapsed #sidebar { width: var(--sidebar-collapsed); flex-basis: var(--sidebar-collapsed); }
|
|
178
|
+
|
|
179
|
+
#brand-row {
|
|
180
|
+
height: 60px; flex: 0 0 60px; padding: 0 14px 0 16px; display: flex; align-items: center;
|
|
181
|
+
justify-content: space-between; gap: 8px;
|
|
182
|
+
}
|
|
183
|
+
#brand { min-width: 0; display: flex; align-items: center; gap: 10px; }
|
|
184
|
+
#brand-mark {
|
|
185
|
+
display: grid; width: 30px; height: 30px; flex: none; place-items: center;
|
|
186
|
+
background: var(--fg); color: var(--bg); border-radius: var(--radius-sm);
|
|
187
|
+
font: 500 16px/1 var(--font-display);
|
|
188
|
+
}
|
|
189
|
+
#brand-name {
|
|
190
|
+
font: 650 15px/1.1 var(--font-display); letter-spacing: -0.01em; color: var(--fg); white-space: nowrap;
|
|
191
|
+
}
|
|
192
|
+
.icon-button {
|
|
193
|
+
display: grid; place-items: center; width: 32px; height: 32px; padding: 0; flex: none;
|
|
194
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer; border-radius: var(--radius-sm);
|
|
195
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
196
|
+
}
|
|
197
|
+
.icon-button:hover { color: var(--fg); background: var(--surface-warm); }
|
|
198
|
+
#mobile-close { display: none; }
|
|
199
|
+
body.sidebar-collapsed #brand-row { justify-content: center; padding: 0; }
|
|
200
|
+
body.sidebar-collapsed #brand-name, body.sidebar-collapsed .sidebar-copy { display: none; }
|
|
201
|
+
body.sidebar-collapsed #sidebar-toggle { display: none; }
|
|
202
|
+
body.sidebar-collapsed #brand-mark { cursor: pointer; }
|
|
203
|
+
|
|
204
|
+
#new-session {
|
|
205
|
+
display: flex; align-items: center; gap: 10px; margin: 2px 12px 4px; width: calc(100% - 24px);
|
|
206
|
+
padding: 9px 12px; border: 1px solid var(--border-soft); border-radius: var(--radius-md);
|
|
207
|
+
background: var(--bg); color: var(--fg-2); cursor: pointer; text-align: left;
|
|
208
|
+
font: 500 13px/1 var(--font-body); letter-spacing: 0.01em;
|
|
209
|
+
transition: background var(--motion-fast), border-color var(--motion-fast);
|
|
210
|
+
}
|
|
211
|
+
#new-session:hover { background: var(--surface-warm); border-color: var(--border-soft); color: var(--fg); }
|
|
212
|
+
body.sidebar-collapsed #new-session { justify-content: center; padding-inline: 0; }
|
|
213
|
+
|
|
214
|
+
.nav-section-label {
|
|
215
|
+
padding: 16px 20px 6px; color: var(--muted);
|
|
216
|
+
font: 500 10px/1 var(--font-body); letter-spacing: 0.08em; text-transform: uppercase;
|
|
217
|
+
}
|
|
218
|
+
body.sidebar-collapsed .nav-section-label { height: 9px; padding: 0; overflow: hidden; }
|
|
219
|
+
|
|
220
|
+
#session-list { min-height: 0; flex: 1; overflow: auto; padding: 0 0 8px; }
|
|
221
|
+
|
|
222
|
+
/* Workspace group headers in session list */
|
|
223
|
+
.ws-group {
|
|
224
|
+
margin-top: 4px;
|
|
225
|
+
}
|
|
226
|
+
.ws-group-header {
|
|
227
|
+
display: flex; align-items: center; gap: 6px;
|
|
228
|
+
padding: 8px 14px 4px; margin: 0 8px;
|
|
229
|
+
color: var(--muted); cursor: pointer; user-select: none;
|
|
230
|
+
font: 500 11px/1 var(--font-body); letter-spacing: 0.03em;
|
|
231
|
+
border-radius: var(--radius-sm);
|
|
232
|
+
transition: color var(--motion-fast);
|
|
233
|
+
}
|
|
234
|
+
.ws-group-header:hover { color: var(--fg-2); }
|
|
235
|
+
.ws-group-header .ws-chev {
|
|
236
|
+
font-size: 8px; transition: transform 0.15s;
|
|
237
|
+
}
|
|
238
|
+
.ws-group-header.collapsed .ws-chev { transform: rotate(-90deg); }
|
|
239
|
+
.ws-group-header .ws-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
240
|
+
.ws-group-header .ws-count { color: var(--muted); font-size: 10px; }
|
|
241
|
+
.ws-group-sessions { }
|
|
242
|
+
.ws-group-header.collapsed + .ws-group-sessions { display: none; }
|
|
243
|
+
|
|
244
|
+
/* Sidebar parallel tabs */
|
|
245
|
+
#sidebar-tabs { padding: 0 0 4px; }
|
|
246
|
+
.sidebar-tab {
|
|
247
|
+
position: relative; display: flex; align-items: center; gap: 8px; min-height: 34px;
|
|
248
|
+
margin: 1px 8px; padding: 7px 10px 7px 14px; border-radius: var(--radius-sm);
|
|
249
|
+
color: var(--fg-2); cursor: pointer; font: 13px/1.35 var(--font-body); white-space: nowrap;
|
|
250
|
+
transition: background var(--motion-fast);
|
|
251
|
+
}
|
|
252
|
+
.sidebar-tab::before {
|
|
253
|
+
content: ""; position: absolute; left: 5px; top: 50%; transform: translateY(-50%);
|
|
254
|
+
width: 5px; height: 5px; flex: none; border-radius: 50%; background: var(--border-soft);
|
|
255
|
+
}
|
|
256
|
+
.sidebar-tab:hover { background: var(--tint-hover); }
|
|
257
|
+
.sidebar-tab.active { background: var(--surface-warm); color: var(--fg); font-weight: 500; }
|
|
258
|
+
.sidebar-tab.active::before { background: var(--accent); }
|
|
259
|
+
.sidebar-tab.streaming::before { background: var(--warn); animation: pulse 1.4s infinite; }
|
|
260
|
+
.sidebar-tab span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
261
|
+
.sidebar-tab .tab-close-btn {
|
|
262
|
+
display: grid; width: 22px; height: 22px; padding: 0; flex: none; place-items: center;
|
|
263
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer; border-radius: 5px;
|
|
264
|
+
font: 14px/1 var(--font-body); opacity: 0;
|
|
265
|
+
transition: opacity var(--motion-fast), color var(--motion-fast), background var(--motion-fast);
|
|
266
|
+
}
|
|
267
|
+
.sidebar-tab:hover .tab-close-btn, .sidebar-tab .tab-close-btn:focus-visible { opacity: 1; }
|
|
268
|
+
.sidebar-tab .tab-close-btn:hover { background: color-mix(in oklab, var(--danger) 10%, transparent); color: var(--danger); }
|
|
269
|
+
#sidebar-tab-add {
|
|
270
|
+
display: flex; align-items: center; gap: 10px; margin: 2px 8px; padding: 5px 10px 5px 14px;
|
|
271
|
+
border: 0; border-radius: var(--radius-sm); background: transparent; width: calc(100% - 16px);
|
|
272
|
+
color: var(--muted); cursor: pointer; font: 12px/1 var(--font-body); text-align: left;
|
|
273
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
274
|
+
}
|
|
275
|
+
#sidebar-tab-add:hover { color: var(--fg-2); background: var(--tint-hover); }
|
|
276
|
+
body.sidebar-collapsed #sidebar-tabs { display: none; }
|
|
277
|
+
.sess {
|
|
278
|
+
position: relative; display: flex; align-items: center; gap: 8px; min-height: 34px;
|
|
279
|
+
margin: 1px 8px; padding: 7px 10px 7px 14px; border-radius: var(--radius-sm);
|
|
280
|
+
color: var(--fg-2); cursor: pointer; font: 13px/1.35 var(--font-body); white-space: nowrap;
|
|
281
|
+
transition: background var(--motion-fast);
|
|
282
|
+
}
|
|
283
|
+
.sess::before {
|
|
284
|
+
content: ""; position: absolute; left: 5px; top: 50%; transform: translateY(-50%);
|
|
285
|
+
width: 5px; height: 5px; flex: none; border-radius: 50%; background: var(--border-soft);
|
|
286
|
+
}
|
|
287
|
+
.sess span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
288
|
+
.session-delete {
|
|
289
|
+
display: grid; width: 24px; height: 24px; padding: 0; flex: none; place-items: center;
|
|
290
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer; border-radius: 6px;
|
|
291
|
+
font: 16px/1 var(--font-body); opacity: 0;
|
|
292
|
+
transition: opacity var(--motion-fast), color var(--motion-fast), background var(--motion-fast);
|
|
293
|
+
}
|
|
294
|
+
.sess:hover .session-delete, .session-delete:focus-visible { opacity: 1; }
|
|
295
|
+
.session-delete:hover { background: color-mix(in oklab, var(--danger) 10%, transparent); color: var(--danger); }
|
|
296
|
+
.session-delete:disabled { cursor: wait; opacity: 0.45; }
|
|
297
|
+
.sess:hover { background: var(--tint-hover); }
|
|
298
|
+
.sess.current { background: var(--surface-warm); color: var(--fg); font-weight: 500; }
|
|
299
|
+
.sess.current::before { background: var(--accent); }
|
|
300
|
+
body.sidebar-collapsed .sess { justify-content: center; padding-inline: 0; margin: 1px 10px; }
|
|
301
|
+
body.sidebar-collapsed .sess::before { left: 50%; transform: translate(-50%, -50%); }
|
|
302
|
+
body.sidebar-collapsed .sess.current::before { left: 50%; }
|
|
303
|
+
body.sidebar-collapsed .sess span, body.sidebar-collapsed .session-delete { display: none; }
|
|
304
|
+
@media (hover: none) { .session-delete { opacity: 0.6; } }
|
|
305
|
+
|
|
306
|
+
#side-system { flex: none; border-top: 1px solid var(--border-soft); padding: 6px 0 4px; }
|
|
307
|
+
.side-action {
|
|
308
|
+
display: flex; align-items: center; gap: 10px; width: 100%;
|
|
309
|
+
margin: 1px 0; padding: 9px 20px; border: 0; background: transparent; color: var(--fg-2);
|
|
310
|
+
cursor: pointer; text-align: left; font: 500 13px/1 var(--font-body);
|
|
311
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
312
|
+
}
|
|
313
|
+
.side-action:hover { color: var(--fg); background: var(--tint-hover); }
|
|
314
|
+
#gateway-dot {
|
|
315
|
+
width: 7px; height: 7px; margin-left: auto; border-radius: 50%; background: var(--success);
|
|
316
|
+
}
|
|
317
|
+
body.running #gateway-dot { background: var(--warn); animation: pulse 1.4s infinite; }
|
|
318
|
+
body.sidebar-collapsed .side-action { justify-content: center; padding-inline: 0; }
|
|
319
|
+
body.sidebar-collapsed #gateway-dot { position: absolute; right: 12px; bottom: 8px; width: 5px; height: 5px; }
|
|
320
|
+
|
|
321
|
+
#sidebar-footer {
|
|
322
|
+
min-height: 44px; flex: none; padding: 10px 20px; border-top: 1px solid var(--border-soft);
|
|
323
|
+
color: var(--muted); font: 11px/1.5 var(--font-mono);
|
|
324
|
+
}
|
|
325
|
+
#sidebar-footer b { color: var(--muted); font-weight: 500; }
|
|
326
|
+
body.sidebar-collapsed #sidebar-footer { display: none; }
|
|
327
|
+
|
|
328
|
+
/* ─── Main column ─────────────────────────────────────────────────── */
|
|
329
|
+
#main { position: relative; min-width: 0; min-height: 0; flex: 1; display: flex; flex-direction: column; background: var(--bg); }
|
|
330
|
+
#mobile-header { display: none; }
|
|
331
|
+
|
|
332
|
+
#conv-header {
|
|
333
|
+
min-height: 64px; flex: 0 0 auto; display: flex; align-items: center; gap: 12px;
|
|
334
|
+
padding: 10px 24px; border-bottom: 1px solid var(--border-soft); background: var(--bg);
|
|
335
|
+
}
|
|
336
|
+
#heading-wrap { min-width: 0; flex: 1; }
|
|
337
|
+
#eyebrow {
|
|
338
|
+
margin-bottom: 2px; color: var(--muted);
|
|
339
|
+
font: 500 10px/1 var(--font-body); letter-spacing: 0.08em; text-transform: uppercase;
|
|
340
|
+
}
|
|
341
|
+
#conv-title {
|
|
342
|
+
overflow: hidden; color: var(--fg); font: 650 18px/1.2 var(--font-display);
|
|
343
|
+
letter-spacing: -0.02em; text-overflow: ellipsis; white-space: nowrap;
|
|
344
|
+
}
|
|
345
|
+
#ws-chip, #model-chip {
|
|
346
|
+
max-width: min(30vw, 360px); display: flex; align-items: center; gap: 8px; padding: 7px 12px;
|
|
347
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--surface);
|
|
348
|
+
color: var(--muted); cursor: pointer; font: 12px/1.2 var(--font-mono);
|
|
349
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
350
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
351
|
+
}
|
|
352
|
+
#model-chip { max-width: 220px; }
|
|
353
|
+
#ws-chip span, #model-chip span { overflow: hidden; text-overflow: ellipsis; }
|
|
354
|
+
#ws-chip:hover, #model-chip:hover { color: var(--fg); background: var(--surface-warm); }
|
|
355
|
+
|
|
356
|
+
.view-tabs { align-self: stretch; display: flex; gap: 2px; }
|
|
357
|
+
.view-tabs button {
|
|
358
|
+
position: relative; padding: 0 13px; border: 0; background: transparent;
|
|
359
|
+
color: var(--muted); cursor: pointer; border-radius: var(--radius-sm);
|
|
360
|
+
font: 500 12px/1 var(--font-body); letter-spacing: 0.03em;
|
|
361
|
+
transition: color var(--motion-fast);
|
|
362
|
+
}
|
|
363
|
+
.view-tabs button:hover { color: var(--fg-2); }
|
|
364
|
+
.view-tabs button.active { color: var(--fg); }
|
|
365
|
+
.view-tabs button.active::after {
|
|
366
|
+
content: ""; position: absolute; left: 10px; right: 10px; bottom: -13px; height: 2px;
|
|
367
|
+
background: var(--fg); border-radius: 1px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#status-pill {
|
|
371
|
+
display: flex; align-items: center; gap: 7px; padding: 6px 12px;
|
|
372
|
+
border-radius: var(--radius-pill); background: var(--surface); border: 1px solid var(--border-soft);
|
|
373
|
+
color: var(--muted); font: 500 11px/1 var(--font-body); letter-spacing: 0.04em; text-transform: capitalize;
|
|
374
|
+
}
|
|
375
|
+
#status-pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--success); }
|
|
376
|
+
#status-pill.running::before { background: var(--warn); animation: pulse 1.4s infinite; }
|
|
377
|
+
@keyframes pulse { 50% { opacity: 0.35; } }
|
|
378
|
+
|
|
379
|
+
#views { position: relative; flex: 1; min-height: 0; }
|
|
380
|
+
.view { position: absolute; inset: 0; display: none; overflow: hidden; }
|
|
381
|
+
.view.active { display: flex; flex-direction: column; }
|
|
382
|
+
/* ─── Parallel session tab strip — hidden, merged into sidebar ──── */
|
|
383
|
+
#tab-bar {
|
|
384
|
+
display: none;
|
|
385
|
+
}
|
|
386
|
+
#tab-bar::-webkit-scrollbar { height: 3px; }
|
|
387
|
+
.tab-item {
|
|
388
|
+
display: inline-flex; align-items: center; gap: 8px; max-width: 200px;
|
|
389
|
+
padding: 7px 10px 7px 12px; border: 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
390
|
+
background: transparent; color: var(--muted); cursor: pointer;
|
|
391
|
+
font: 500 13px/1.2 var(--font-body); white-space: nowrap;
|
|
392
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
393
|
+
}
|
|
394
|
+
.tab-item:hover { color: var(--fg-2); background: rgba(20, 20, 19, 0.04); }
|
|
395
|
+
.tab-item.active { color: var(--fg); background: var(--surface); box-shadow: inset 0 -2px 0 var(--fg); }
|
|
396
|
+
.tab-item .tab-label { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
397
|
+
.tab-item .tab-dot { width: 6px; height: 6px; flex: none; border-radius: 50%; background: var(--border-soft); }
|
|
398
|
+
.tab-item.streaming .tab-dot { background: var(--warn); animation: pulse 1.4s infinite; }
|
|
399
|
+
.tab-item .tab-close {
|
|
400
|
+
display: grid; place-items: center; width: 18px; height: 18px; padding: 0; flex: none;
|
|
401
|
+
border: 0; border-radius: 5px; background: transparent; color: var(--muted);
|
|
402
|
+
font: 14px/1 var(--font-body); cursor: pointer; opacity: 0;
|
|
403
|
+
transition: opacity var(--motion-fast), color var(--motion-fast), background var(--motion-fast);
|
|
404
|
+
}
|
|
405
|
+
.tab-item:hover .tab-close, .tab-close:focus-visible { opacity: 1; }
|
|
406
|
+
.tab-close:hover { color: var(--danger); background: color-mix(in oklab, var(--danger) 10%, transparent); }
|
|
407
|
+
#tab-add {
|
|
408
|
+
display: grid; place-items: center; width: 26px; height: 26px; flex: none;
|
|
409
|
+
border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--muted);
|
|
410
|
+
font: 400 17px/1 var(--font-body); cursor: pointer;
|
|
411
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
412
|
+
}
|
|
413
|
+
#tab-add:hover { color: var(--fg-2); background: rgba(20, 20, 19, 0.04); }
|
|
414
|
+
|
|
415
|
+
/* Per-tab chat containers — one scroller child per open tab */
|
|
416
|
+
.tab-chat-container {
|
|
417
|
+
display: flex; flex-direction: column; gap: 20px; width: 100%;
|
|
418
|
+
flex: 1; min-height: 0; overflow-y: auto; scroll-behavior: smooth;
|
|
419
|
+
padding: 24px max(28px, calc((100% - var(--chat-max)) / 2));
|
|
420
|
+
}
|
|
421
|
+
.tab-chat-container > * { flex: 0 0 auto; }
|
|
422
|
+
.tab-chat-container:empty::before {
|
|
423
|
+
content: "Start a session with Pi"; margin: auto; color: var(--muted);
|
|
424
|
+
font: 600 var(--text-xl)/1.2 var(--font-display); letter-spacing: var(--tracking-display);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* ─── Conversation rows ───────────────────────────────────────────── */
|
|
428
|
+
.row-user, .row-assistant { position: relative; max-width: min(80%, 720px); word-break: break-word; }
|
|
429
|
+
.row-user {
|
|
430
|
+
align-self: flex-end; padding: 12px 16px;
|
|
431
|
+
background: var(--surface-warm); color: var(--fg);
|
|
432
|
+
border-radius: var(--radius-md) var(--radius-md) 4px var(--radius-md);
|
|
433
|
+
white-space: pre-wrap;
|
|
434
|
+
}
|
|
435
|
+
.row-user::before, .row-assistant::before {
|
|
436
|
+
display: block; margin-bottom: 6px; color: var(--muted);
|
|
437
|
+
font: 500 9px/1 var(--font-body); letter-spacing: 0.1em; text-transform: uppercase;
|
|
438
|
+
}
|
|
439
|
+
.row-user::before { content: "You"; text-align: right; }
|
|
440
|
+
.row-assistant {
|
|
441
|
+
align-self: flex-start; padding: 0; color: var(--fg); white-space: normal; overflow-wrap: anywhere;
|
|
442
|
+
}
|
|
443
|
+
.row-assistant::before { content: "Pi"; }
|
|
444
|
+
.row-assistant > :first-child { margin-top: 0; }
|
|
445
|
+
.row-assistant > :last-child { margin-bottom: 0; }
|
|
446
|
+
.row-assistant p { margin: 0.65em 0; }
|
|
447
|
+
.row-assistant h1, .row-assistant h2, .row-assistant h3,
|
|
448
|
+
.row-assistant h4, .row-assistant h5, .row-assistant h6 {
|
|
449
|
+
margin: 1.2em 0 0.5em; color: var(--fg);
|
|
450
|
+
font-family: var(--font-display); font-weight: 650; line-height: 1.25; letter-spacing: -0.015em;
|
|
451
|
+
}
|
|
452
|
+
.row-assistant h1 { font-size: 1.5em; }
|
|
453
|
+
.row-assistant h2 { font-size: 1.3em; }
|
|
454
|
+
.row-assistant h3 { font-size: 1.12em; }
|
|
455
|
+
.row-assistant h4, .row-assistant h5, .row-assistant h6 { font-size: 1em; }
|
|
456
|
+
.row-assistant ul, .row-assistant ol { margin: 0.65em 0; padding-left: 1.65em; }
|
|
457
|
+
.row-assistant li { margin: 0.25em 0; }
|
|
458
|
+
.row-assistant li > p { margin: 0.25em 0; }
|
|
459
|
+
.row-assistant blockquote {
|
|
460
|
+
margin: 0.8em 0; padding: 0.1em 0 0.1em 1em;
|
|
461
|
+
border-left: 2px solid var(--border-soft); color: var(--muted);
|
|
462
|
+
}
|
|
463
|
+
.row-assistant a { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--muted); }
|
|
464
|
+
.row-assistant a:hover { text-decoration-color: var(--fg); }
|
|
465
|
+
.row-assistant code {
|
|
466
|
+
padding: 0.12em 0.4em; border: 1px solid var(--border-soft); border-radius: 6px;
|
|
467
|
+
background: var(--surface); color: var(--fg-2); font-size: 0.88em;
|
|
468
|
+
}
|
|
469
|
+
.row-assistant pre {
|
|
470
|
+
margin: 0.8em 0; padding: 14px 16px; overflow-x: auto;
|
|
471
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
|
|
472
|
+
background: var(--surface); white-space: pre; word-break: normal; line-height: 1.55;
|
|
473
|
+
}
|
|
474
|
+
.row-assistant pre code { padding: 0; border: 0; background: transparent; color: inherit; font-size: 12px; }
|
|
475
|
+
.row-assistant hr { margin: 1em 0; border: 0; border-top: 1px solid var(--border-soft); }
|
|
476
|
+
.row-assistant table { display: block; margin: 0.8em 0; overflow-x: auto; font-size: 0.9em; }
|
|
477
|
+
.row-assistant input[type="checkbox"] { margin: 0 0.45em 0 0; accent-color: var(--accent); }
|
|
478
|
+
|
|
479
|
+
/* Tool calls & reasoning disclosures */
|
|
480
|
+
details.disclose {
|
|
481
|
+
align-self: stretch; max-width: min(86%, 780px); overflow: hidden;
|
|
482
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--surface);
|
|
483
|
+
}
|
|
484
|
+
details.disclose summary {
|
|
485
|
+
min-height: 38px; display: flex; align-items: center; gap: 10px; padding: 9px 12px;
|
|
486
|
+
color: var(--muted); cursor: pointer; list-style: none; user-select: none;
|
|
487
|
+
font: 12px/1.3 var(--font-mono);
|
|
488
|
+
transition: background var(--motion-fast);
|
|
489
|
+
}
|
|
490
|
+
details.disclose summary::-webkit-details-marker { display: none; }
|
|
491
|
+
details.disclose summary:hover { background: var(--tint-subtle); }
|
|
492
|
+
.chev { font-size: 8px; transition: transform 0.15s; }
|
|
493
|
+
details[open] .chev { transform: rotate(90deg); }
|
|
494
|
+
.tool-name { color: var(--fg-2); font-weight: 600; }
|
|
495
|
+
.tool-args { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
496
|
+
.spin { font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
497
|
+
details.disclose .body {
|
|
498
|
+
max-height: 330px; overflow: auto; padding: 12px 14px; border-top: 1px solid var(--border);
|
|
499
|
+
color: var(--fg-2); white-space: pre-wrap; word-break: break-word;
|
|
500
|
+
font: 12px/1.6 var(--font-mono); background: var(--bg);
|
|
501
|
+
}
|
|
502
|
+
details.disclose.err { border-color: color-mix(in oklab, var(--danger) 35%, transparent); }
|
|
503
|
+
details.disclose.err .tool-name { color: var(--danger); }
|
|
504
|
+
details.think summary, details.think .tool-name { color: var(--muted); font-style: italic; }
|
|
505
|
+
.usage-note {
|
|
506
|
+
align-self: flex-start; margin-top: -12px; color: var(--muted);
|
|
507
|
+
font: 11px/1 var(--font-mono);
|
|
508
|
+
}
|
|
509
|
+
.turn-tail {
|
|
510
|
+
align-self: center; color: var(--muted);
|
|
511
|
+
font: 500 10px/1 var(--font-body); letter-spacing: 0.08em; text-transform: uppercase;
|
|
512
|
+
}
|
|
513
|
+
.note-row, .note-error {
|
|
514
|
+
align-self: center; padding: 8px 14px; border-radius: var(--radius-pill);
|
|
515
|
+
border: 1px solid var(--border-soft); background: var(--surface);
|
|
516
|
+
color: var(--muted); font: 12px/1.4 var(--font-mono); max-width: 90%;
|
|
517
|
+
}
|
|
518
|
+
.note-error { color: var(--danger); border-color: color-mix(in oklab, var(--danger) 35%, transparent); }
|
|
519
|
+
|
|
520
|
+
/* Logs view */
|
|
521
|
+
#view-traj { padding: 24px max(28px, calc((100% - 900px) / 2)); }
|
|
522
|
+
#traj-body { width: 100%; }
|
|
523
|
+
.logline {
|
|
524
|
+
padding: 14px 4px; border-bottom: 1px solid var(--border);
|
|
525
|
+
color: var(--fg-2); font: 12px/1.6 var(--font-mono);
|
|
526
|
+
}
|
|
527
|
+
.logmeta {
|
|
528
|
+
margin-bottom: 5px; color: var(--muted); font-size: 10px;
|
|
529
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* ─── Composer ────────────────────────────────────────────────────── */
|
|
533
|
+
#composer-dock {
|
|
534
|
+
flex: none; padding: 0 max(28px, calc((100% - var(--chat-max)) / 2)) 18px;
|
|
535
|
+
background: linear-gradient(0deg, var(--bg) 75%, transparent);
|
|
536
|
+
}
|
|
537
|
+
#stats-line {
|
|
538
|
+
min-height: 30px; display: flex; align-items: center; gap: 18px; padding: 6px 4px;
|
|
539
|
+
color: var(--muted); font: 11px/1 var(--font-mono); letter-spacing: 0.01em;
|
|
540
|
+
}
|
|
541
|
+
#stats-line b { color: var(--fg-2); font-weight: 500; }
|
|
542
|
+
#ctx-ring {
|
|
543
|
+
min-width: 210px; margin-left: auto; display: grid; grid-template-columns: auto auto 76px auto;
|
|
544
|
+
align-items: center; gap: 8px; cursor: help;
|
|
545
|
+
}
|
|
546
|
+
#ctx-ring b { white-space: nowrap; }
|
|
547
|
+
.context-bar { width: 76px; height: 4px; overflow: hidden; border-radius: 2px; background: var(--border-soft); }
|
|
548
|
+
#ctx-fill {
|
|
549
|
+
display: block; width: 0; height: 100%; border-radius: 2px; background: var(--success);
|
|
550
|
+
transition: width 0.25s ease, background 0.25s ease;
|
|
551
|
+
}
|
|
552
|
+
#ctx-ring.warning #ctx-fill { background: var(--warn); }
|
|
553
|
+
#ctx-ring.danger #ctx-fill { background: var(--danger); }
|
|
554
|
+
#ctx-ring.warning #st-ctx-pct { color: color-mix(in oklab, var(--warn), black 25%); }
|
|
555
|
+
#ctx-ring.danger #st-ctx-pct { color: var(--danger); }
|
|
556
|
+
|
|
557
|
+
#input-shell {
|
|
558
|
+
position: relative; border: 1px solid var(--border-soft); border-radius: var(--radius-lg);
|
|
559
|
+
background: var(--surface); box-shadow: var(--elev-raised);
|
|
560
|
+
transition: border-color var(--motion-base), box-shadow var(--motion-base);
|
|
561
|
+
}
|
|
562
|
+
#input-shell:focus-within {
|
|
563
|
+
border-color: color-mix(in oklab, var(--border-soft), var(--fg) 22%);
|
|
564
|
+
box-shadow: 0 0 0 1px color-mix(in oklab, var(--border-soft), var(--fg) 18%), var(--elev-raised);
|
|
565
|
+
}
|
|
566
|
+
#autocomplete {
|
|
567
|
+
position: absolute; z-index: 25; left: 0; right: 0; bottom: calc(100% + 8px);
|
|
568
|
+
display: none; max-height: min(330px, 42vh); overflow-y: auto;
|
|
569
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-md);
|
|
570
|
+
background: var(--surface); box-shadow: 0 12px 40px rgba(20, 20, 19, 0.12);
|
|
571
|
+
}
|
|
572
|
+
#autocomplete.open { display: block; }
|
|
573
|
+
.completion-item {
|
|
574
|
+
display: grid; grid-template-columns: minmax(120px, 0.65fr) 1fr auto; gap: 14px;
|
|
575
|
+
align-items: center; width: 100%; padding: 10px 14px; border: 0; border-bottom: 1px solid var(--border);
|
|
576
|
+
background: transparent; color: var(--fg-2); cursor: pointer; text-align: left;
|
|
577
|
+
transition: background var(--motion-fast);
|
|
578
|
+
}
|
|
579
|
+
.completion-item:last-child { border-bottom: 0; }
|
|
580
|
+
.completion-item:hover, .completion-item.selected { background: var(--surface-warm); color: var(--fg); }
|
|
581
|
+
.completion-item:first-child { border-radius: var(--radius-md) var(--radius-md) 0 0; }
|
|
582
|
+
.completion-item:last-child { border-radius: 0 0 var(--radius-md) var(--radius-md); }
|
|
583
|
+
.completion-value { overflow: hidden; font: 12px/1.3 var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
|
|
584
|
+
.completion-description { overflow: hidden; color: var(--muted); font: 12px/1.3 var(--font-body); text-overflow: ellipsis; white-space: nowrap; }
|
|
585
|
+
.completion-source { color: var(--muted); font: 500 9px/1 var(--font-body); letter-spacing: 0.07em; text-transform: uppercase; }
|
|
586
|
+
|
|
587
|
+
.search-input {
|
|
588
|
+
width: 100%; margin: 0 0 14px; padding: 10px 12px;
|
|
589
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-md); outline: 0;
|
|
590
|
+
background: var(--bg); color: var(--fg); font: 13px/1.2 var(--font-mono);
|
|
591
|
+
transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
|
|
592
|
+
}
|
|
593
|
+
.search-input:focus { border-color: color-mix(in oklab, var(--border-soft), var(--fg) 22%); box-shadow: var(--focus-ring); }
|
|
594
|
+
.model-count { margin: -5px 0 12px; color: var(--muted); font: 11px/1 var(--font-mono); }
|
|
595
|
+
|
|
596
|
+
#input {
|
|
597
|
+
display: block; width: 100%; min-height: 68px; max-height: 230px; resize: none;
|
|
598
|
+
padding: 14px 16px 6px; border: 0; outline: 0; background: transparent;
|
|
599
|
+
color: var(--fg); caret-color: var(--accent); font: 14px/1.6 var(--font-body);
|
|
600
|
+
}
|
|
601
|
+
#input::placeholder { color: var(--muted); }
|
|
602
|
+
#composer-actions { min-height: 44px; display: flex; align-items: center; gap: 8px; padding: 4px 8px 8px 16px; }
|
|
603
|
+
#composer-hint { flex: 1; color: var(--muted); font: 11px/1 var(--font-mono); }
|
|
604
|
+
|
|
605
|
+
.btn {
|
|
606
|
+
min-height: 34px; padding: 0 16px; border: 0; border-radius: 10px;
|
|
607
|
+
background: var(--accent); color: var(--accent-on); cursor: pointer;
|
|
608
|
+
font: 500 13px/1 var(--font-body); letter-spacing: 0.01em;
|
|
609
|
+
transition: background var(--motion-fast), transform var(--motion-fast);
|
|
610
|
+
}
|
|
611
|
+
.btn:hover { background: var(--accent-hover); }
|
|
612
|
+
.btn:active { background: var(--accent-active); transform: translateY(1px); }
|
|
613
|
+
.btn.secondary {
|
|
614
|
+
border: 1px solid var(--border-soft); background: transparent; color: var(--fg-2);
|
|
615
|
+
}
|
|
616
|
+
.btn.secondary:hover { background: var(--surface-warm); color: var(--fg); }
|
|
617
|
+
.btn.secondary:active { transform: translateY(1px); }
|
|
618
|
+
#steer-btn { display: none; }
|
|
619
|
+
body.running #steer-btn { display: block; }
|
|
620
|
+
|
|
621
|
+
/* ─── Inspector panel ─────────────────────────────────────────────── */
|
|
622
|
+
#details {
|
|
623
|
+
width: 0; flex: 0 0 0; display: flex; flex-direction: column; min-height: 0; overflow: hidden;
|
|
624
|
+
border-left: 0 solid var(--border-soft); background: var(--surface);
|
|
625
|
+
transition: width 0.25s ease, flex-basis 0.25s ease, border-left-width 0.25s ease;
|
|
626
|
+
}
|
|
627
|
+
#details.open { width: var(--details-width); flex-basis: var(--details-width); border-left-width: 1px; }
|
|
628
|
+
#details-head {
|
|
629
|
+
height: 56px; flex: none; display: flex; align-items: center; justify-content: space-between;
|
|
630
|
+
padding: 0 16px; border-bottom: 1px solid var(--border-soft);
|
|
631
|
+
font: 650 14px/1.2 var(--font-display); letter-spacing: -0.01em; color: var(--fg); white-space: nowrap;
|
|
632
|
+
}
|
|
633
|
+
#details-body {
|
|
634
|
+
flex: 1; overflow: auto; padding: 16px;
|
|
635
|
+
font: 12px/1.6 var(--font-mono); white-space: pre-wrap; word-break: break-word; color: var(--fg-2);
|
|
636
|
+
}
|
|
637
|
+
#details-close {
|
|
638
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer;
|
|
639
|
+
font: 20px/1 var(--font-body); border-radius: 6px; width: 28px; height: 28px;
|
|
640
|
+
}
|
|
641
|
+
#details-close:hover { color: var(--fg); background: var(--surface-warm); }
|
|
642
|
+
|
|
643
|
+
/* ─── Floating dock stack — activity + git tiles hover over the chat ── */
|
|
644
|
+
#dock-stack {
|
|
645
|
+
position: absolute; top: 20px; right: 20px; z-index: 20;
|
|
646
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 12px;
|
|
647
|
+
max-height: calc(100% - 40px); pointer-events: none;
|
|
648
|
+
}
|
|
649
|
+
#dock-stack > * { pointer-events: auto; }
|
|
650
|
+
#activity-dock {
|
|
651
|
+
position: relative;
|
|
652
|
+
width: 300px; min-height: 0;
|
|
653
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
654
|
+
background: var(--surface);
|
|
655
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-md);
|
|
656
|
+
box-shadow: var(--elev-raised);
|
|
657
|
+
}
|
|
658
|
+
#activity-dock.minimized { width: auto; max-width: 250px; }
|
|
659
|
+
#activity-toggle {
|
|
660
|
+
display: flex; align-items: center; gap: 8px; width: 100%; flex: none;
|
|
661
|
+
padding: 12px 14px; border: 0; background: transparent; cursor: pointer;
|
|
662
|
+
font: 500 13px/1 var(--font-body); letter-spacing: 0.01em; color: var(--fg); text-align: left;
|
|
663
|
+
transition: background var(--motion-fast);
|
|
664
|
+
}
|
|
665
|
+
#activity-toggle:hover { background: var(--tint-subtle); }
|
|
666
|
+
#activity-toggle .act-live {
|
|
667
|
+
flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--muted);
|
|
668
|
+
transition: background var(--motion-base);
|
|
669
|
+
}
|
|
670
|
+
#activity-dock.live #activity-toggle .act-live { background: var(--success); }
|
|
671
|
+
#activity-dock.live #activity-toggle .act-live { animation: act-pulse 1.6s ease-in-out infinite; }
|
|
672
|
+
@keyframes act-pulse { 50% { opacity: 0.35; } }
|
|
673
|
+
#activity-toggle .act-title { flex: 1; }
|
|
674
|
+
#activity-toggle .act-count { font: 500 11px/1 var(--font-mono); letter-spacing: 0.02em; color: var(--muted); }
|
|
675
|
+
#activity-toggle .act-chev { display: grid; place-items: center; color: var(--muted); transition: transform var(--motion-fast) var(--ease-standard); }
|
|
676
|
+
#activity-toggle .act-chev .icon { width: 12px; height: 12px; }
|
|
677
|
+
#activity-dock.minimized #activity-toggle .act-chev { transform: rotate(180deg); }
|
|
678
|
+
#activity-body {
|
|
679
|
+
flex: 1; min-height: 0; overflow: auto;
|
|
680
|
+
border-top: 1px solid var(--border); padding: 6px 14px 14px;
|
|
681
|
+
}
|
|
682
|
+
#activity-dock.minimized #activity-body { display: none; }
|
|
683
|
+
.act-sec + .act-sec { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
|
|
684
|
+
.act-label {
|
|
685
|
+
margin-bottom: 6px; color: var(--muted);
|
|
686
|
+
font: 500 10px/1 var(--font-body); letter-spacing: 0.08em; text-transform: uppercase;
|
|
687
|
+
}
|
|
688
|
+
.act-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; font: 13px/1.5 var(--font-body); color: var(--fg-2); }
|
|
689
|
+
.act-row .icon { flex: none; width: 14px; height: 14px; color: var(--muted); }
|
|
690
|
+
.act-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
691
|
+
#act-task { font: 13px/1.55 var(--font-body); color: var(--fg-2); overflow-wrap: break-word; }
|
|
692
|
+
#act-stats { margin-top: 8px; font: 11px/1 var(--font-mono); letter-spacing: 0.02em; color: var(--muted); }
|
|
693
|
+
#act-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
694
|
+
#act-steps li { display: flex; align-items: flex-start; gap: 8px; padding: 3px 0; font: 12.5px/1.5 var(--font-body); color: var(--muted); }
|
|
695
|
+
#act-steps li .act-check { flex: none; display: grid; place-items: center; width: 14px; height: 14px; margin-top: 2px; }
|
|
696
|
+
#act-steps li .act-check .icon { width: 13px; height: 13px; }
|
|
697
|
+
#act-steps li.done .act-check { color: var(--success); }
|
|
698
|
+
#act-steps li.err { color: var(--danger); }
|
|
699
|
+
#act-steps li.err .act-check { color: var(--danger); }
|
|
700
|
+
#act-steps li.run .act-name { color: var(--fg); }
|
|
701
|
+
#act-steps li.run .act-check { color: var(--fg-2); animation: act-pulse 1.2s ease-in-out infinite; }
|
|
702
|
+
#act-steps li.pending .act-check { color: var(--muted); }
|
|
703
|
+
#act-steps li.pending .act-name { color: var(--fg-2); }
|
|
704
|
+
#act-steps li .act-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
705
|
+
#act-empty { font: 13px/1.5 var(--font-body); color: var(--muted); }
|
|
706
|
+
|
|
707
|
+
/* ─── Git tile — branch, working-tree changes, commit ────────────── */
|
|
708
|
+
#git-dock {
|
|
709
|
+
position: relative;
|
|
710
|
+
width: 300px; display: flex; flex-direction: column; overflow: hidden;
|
|
711
|
+
background: var(--surface);
|
|
712
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-md);
|
|
713
|
+
box-shadow: var(--elev-raised);
|
|
714
|
+
}
|
|
715
|
+
#git-dock[hidden] { display: none; }
|
|
716
|
+
#git-toggle {
|
|
717
|
+
display: flex; align-items: center; gap: 8px; width: 100%; flex: none;
|
|
718
|
+
padding: 12px 14px; border: 0; background: transparent; cursor: pointer;
|
|
719
|
+
font: 600 13px/1 var(--font-body); letter-spacing: 0.01em; color: var(--fg); text-align: left;
|
|
720
|
+
transition: background var(--motion-fast);
|
|
721
|
+
}
|
|
722
|
+
#git-toggle:hover { background: var(--tint-subtle); }
|
|
723
|
+
#git-toggle > .icon { flex: none; color: var(--muted); }
|
|
724
|
+
#git-toggle .git-repo {
|
|
725
|
+
margin-left: auto; max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
726
|
+
color: var(--muted); font: 500 11px/1 var(--font-mono);
|
|
727
|
+
}
|
|
728
|
+
#git-body { border-top: 1px solid var(--border); padding: 6px; display: flex; flex-direction: column; gap: 1px; }
|
|
729
|
+
#git-dock.minimized #git-body { display: none; }
|
|
730
|
+
.git-row {
|
|
731
|
+
display: flex; align-items: center; gap: 8px; width: 100%;
|
|
732
|
+
padding: 8px; border: 0; border-radius: var(--radius-sm); background: transparent;
|
|
733
|
+
color: var(--fg-2); cursor: pointer; text-align: left;
|
|
734
|
+
font: 500 13px/1.3 var(--font-body);
|
|
735
|
+
transition: background var(--motion-fast);
|
|
736
|
+
}
|
|
737
|
+
.git-row:hover { background: var(--tint-hover); }
|
|
738
|
+
.git-row > .icon { flex: none; width: 15px; height: 15px; color: var(--muted); }
|
|
739
|
+
.git-row .git-row-label { color: var(--muted); }
|
|
740
|
+
.git-row b { margin-left: auto; font: 500 12px/1 var(--font-mono); letter-spacing: 0.01em; white-space: nowrap; }
|
|
741
|
+
.git-row b#git-adds { color: var(--success); }
|
|
742
|
+
.git-row b#git-dels { color: var(--danger); margin-left: 0; }
|
|
743
|
+
.git-row .git-chev { flex: none; color: var(--muted); }
|
|
744
|
+
.git-row .git-chev .icon { width: 12px; height: 12px; color: inherit; }
|
|
745
|
+
.git-commit {
|
|
746
|
+
margin-top: 4px; min-height: 34px; padding: 0 12px;
|
|
747
|
+
border: 0; border-radius: var(--radius-sm);
|
|
748
|
+
background: var(--accent); color: var(--accent-on); cursor: pointer;
|
|
749
|
+
font: 550 13px/1 var(--font-body);
|
|
750
|
+
transition: background var(--motion-fast), transform var(--motion-fast);
|
|
751
|
+
}
|
|
752
|
+
.git-commit:hover { background: var(--accent-hover); }
|
|
753
|
+
.git-commit:active { background: var(--accent-active); transform: translateY(1px); }
|
|
754
|
+
.git-commit:disabled { opacity: 0.5; cursor: wait; transform: none; }
|
|
755
|
+
.diffstat-add { color: var(--success); font-weight: 550; }
|
|
756
|
+
.diffstat-del { color: var(--danger); font-weight: 550; }
|
|
757
|
+
|
|
758
|
+
/* Commit / branches modal forms */
|
|
759
|
+
.modal-textarea {
|
|
760
|
+
width: 100%; min-height: 88px; resize: vertical; margin: 0 0 14px; padding: 10px 12px;
|
|
761
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm); outline: 0;
|
|
762
|
+
background: var(--bg); color: var(--fg); font: 13px/1.5 var(--font-mono);
|
|
763
|
+
transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
|
|
764
|
+
}
|
|
765
|
+
.modal-textarea:focus { border-color: color-mix(in oklab, var(--border-soft), var(--fg) 22%); box-shadow: var(--focus-ring); }
|
|
766
|
+
|
|
767
|
+
/* Selective staging + per-file diffs + history strip (roadmap 2.5.4–2.5.8) */
|
|
768
|
+
.stage-cb { width: 14px; height: 14px; margin: 0; accent-color: var(--accent); cursor: pointer; }
|
|
769
|
+
.file-diff-btn {
|
|
770
|
+
padding: 2px 10px; border: 1px solid var(--border-soft); border-radius: var(--radius-pill);
|
|
771
|
+
background: transparent; color: var(--fg-2); cursor: pointer;
|
|
772
|
+
font: 500 11px/1.4 var(--font-body); white-space: nowrap;
|
|
773
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
774
|
+
}
|
|
775
|
+
.file-diff-btn:hover { background: var(--surface-warm); color: var(--fg); }
|
|
776
|
+
.branch-delete {
|
|
777
|
+
padding: 2px 10px; border: 0; border-radius: var(--radius-pill); background: transparent;
|
|
778
|
+
color: var(--muted); cursor: pointer; font: 500 11px/1.4 var(--font-body);
|
|
779
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
780
|
+
}
|
|
781
|
+
.branch-delete:hover { background: color-mix(in oklab, var(--danger) 10%, transparent); color: var(--danger); }
|
|
782
|
+
.inline-form { display: flex; gap: 8px; margin-top: 12px; }
|
|
783
|
+
.inline-form input {
|
|
784
|
+
flex: 1; min-width: 0; padding: 8px 12px; border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
|
|
785
|
+
outline: 0; background: var(--bg); color: var(--fg); font: 13px/1.2 var(--font-mono);
|
|
786
|
+
}
|
|
787
|
+
.inline-form input:focus { border-color: color-mix(in oklab, var(--border-soft), var(--fg) 22%); box-shadow: var(--focus-ring); }
|
|
788
|
+
|
|
789
|
+
/* Git modal tab strip */
|
|
790
|
+
.git-tabs {
|
|
791
|
+
display: flex; gap: 2px; margin: 0 0 18px; padding: 4px;
|
|
792
|
+
background: var(--bg); border-radius: var(--radius-sm); border: 1px solid var(--border-soft);
|
|
793
|
+
}
|
|
794
|
+
.git-tabs button {
|
|
795
|
+
flex: 1; padding: 8px 12px; border: 0; border-radius: calc(var(--radius-sm) - 2px);
|
|
796
|
+
background: transparent; color: var(--muted); cursor: pointer;
|
|
797
|
+
font: 500 12px/1 var(--font-body); letter-spacing: 0.02em;
|
|
798
|
+
transition: background var(--motion-fast), color var(--motion-fast);
|
|
799
|
+
}
|
|
800
|
+
.git-tabs button:hover { color: var(--fg-2); }
|
|
801
|
+
.git-tabs button.active { background: var(--surface-warm); color: var(--fg); font-weight: 600; }
|
|
802
|
+
.sync-output {
|
|
803
|
+
max-height: 160px; overflow: auto; margin: 12px 0 0; padding: 10px 12px;
|
|
804
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--bg);
|
|
805
|
+
color: var(--fg-2); font: 11px/1.6 var(--font-mono); white-space: pre-wrap; word-break: break-word;
|
|
806
|
+
}
|
|
807
|
+
.diff-view {
|
|
808
|
+
overflow: auto; max-height: 420px; margin-top: 12px; padding: 12px 14px;
|
|
809
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm); background: var(--bg);
|
|
810
|
+
font: 11.5px/1.65 var(--font-mono); white-space: pre;
|
|
811
|
+
}
|
|
812
|
+
.dl { display: block; color: var(--muted); }
|
|
813
|
+
.dl.add { background: color-mix(in oklab, var(--success) 12%, transparent); color: var(--fg); }
|
|
814
|
+
.dl.del { background: color-mix(in oklab, var(--danger) 10%, transparent); color: var(--fg); }
|
|
815
|
+
.dl.hunk { color: var(--meta); margin-top: 6px; }
|
|
816
|
+
.commit-subject { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
817
|
+
|
|
818
|
+
/* Pinned sessions (roadmap 1.3) */
|
|
819
|
+
.session-pin {
|
|
820
|
+
display: grid; width: 24px; height: 24px; padding: 0; flex: none; place-items: center;
|
|
821
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer; border-radius: 6px;
|
|
822
|
+
opacity: 0;
|
|
823
|
+
transition: opacity var(--motion-fast), color var(--motion-fast), background var(--motion-fast);
|
|
824
|
+
}
|
|
825
|
+
.sess:hover .session-pin, .session-pin:focus-visible, .session-pin.pinned { opacity: 1; }
|
|
826
|
+
.session-pin:hover { background: var(--tint-hover); color: var(--fg); }
|
|
827
|
+
.session-pin.pinned { color: var(--meta); }
|
|
828
|
+
.session-pin .icon { width: 13px; height: 13px; }
|
|
829
|
+
|
|
830
|
+
/* ─── Modal dialogs ───────────────────────────────────────────────── */
|
|
831
|
+
#mask { position: fixed; inset: 0; z-index: 60; display: none; background: rgba(20, 20, 19, 0.32); backdrop-filter: blur(2px); }
|
|
832
|
+
#mask.open { display: block; }
|
|
833
|
+
#modal {
|
|
834
|
+
position: fixed; z-index: 70; inset: 50% auto auto 50%; width: min(840px, calc(100vw - 34px));
|
|
835
|
+
height: min(600px, calc(100dvh - 48px)); display: none; grid-template-columns: 184px 1fr;
|
|
836
|
+
overflow: hidden; transform: translate(-50%, -50%);
|
|
837
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-lg);
|
|
838
|
+
background: var(--surface); box-shadow: 0 24px 80px rgba(20, 20, 19, 0.22);
|
|
839
|
+
}
|
|
840
|
+
#modal.open { display: grid; animation: dialog-in 0.16s ease-out; }
|
|
841
|
+
#modal.no-nav { grid-template-columns: 1fr; }
|
|
842
|
+
#modal.no-nav nav { display: none; }
|
|
843
|
+
@keyframes dialog-in { from { opacity: 0; transform: translate(-50%, calc(-50% + 6px)) scale(0.99); } }
|
|
844
|
+
#modal nav {
|
|
845
|
+
display: flex; flex-direction: column; gap: 2px; padding: 14px 0;
|
|
846
|
+
border-right: 1px solid var(--border-soft); background: var(--bg);
|
|
847
|
+
}
|
|
848
|
+
#modal nav::before {
|
|
849
|
+
content: "Settings"; padding: 3px 18px 14px; color: var(--fg);
|
|
850
|
+
font: 650 14px/1.2 var(--font-display); letter-spacing: -0.01em;
|
|
851
|
+
}
|
|
852
|
+
#modal nav button {
|
|
853
|
+
position: relative; margin: 0 8px; padding: 9px 12px; border: 0; border-radius: var(--radius-sm);
|
|
854
|
+
background: transparent; color: var(--fg-2); cursor: pointer; text-align: left;
|
|
855
|
+
font: 500 13px/1 var(--font-body);
|
|
856
|
+
transition: background var(--motion-fast);
|
|
857
|
+
}
|
|
858
|
+
#modal nav button:hover { background: var(--tint-hover); }
|
|
859
|
+
#modal nav button.active { background: var(--surface-warm); color: var(--fg); }
|
|
860
|
+
#modal-body { overflow: auto; padding: 26px 30px; }
|
|
861
|
+
h2.sect {
|
|
862
|
+
margin: 26px 0 10px; color: var(--muted);
|
|
863
|
+
font: 500 10px/1 var(--font-body); letter-spacing: 0.08em; text-transform: uppercase;
|
|
864
|
+
}
|
|
865
|
+
h2.sect:first-child { margin-top: 0; }
|
|
866
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
867
|
+
th {
|
|
868
|
+
padding: 8px; border-bottom: 1px solid var(--border-soft); color: var(--muted);
|
|
869
|
+
font: 500 10px/1 var(--font-body); letter-spacing: 0.07em; text-align: left; text-transform: uppercase;
|
|
870
|
+
}
|
|
871
|
+
td {
|
|
872
|
+
padding: 10px 8px; border-bottom: 1px solid var(--border); color: var(--fg-2);
|
|
873
|
+
vertical-align: top; word-break: break-word;
|
|
874
|
+
}
|
|
875
|
+
tr.current td { color: var(--fg); font-weight: 500; }
|
|
876
|
+
tr.clickable { cursor: pointer; }
|
|
877
|
+
tr.clickable:hover td { color: var(--fg); background: var(--tint-subtle); }
|
|
878
|
+
pre.json {
|
|
879
|
+
padding: 14px; overflow: auto; border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
|
|
880
|
+
background: var(--bg); color: var(--fg-2); white-space: pre-wrap; word-break: break-word; font-size: 11px;
|
|
881
|
+
}
|
|
882
|
+
select, .chip {
|
|
883
|
+
border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
|
|
884
|
+
background: var(--bg); color: var(--fg-2);
|
|
885
|
+
}
|
|
886
|
+
select { padding: 5px 8px; }
|
|
887
|
+
.crumb { color: var(--fg-2); cursor: pointer; }
|
|
888
|
+
.crumb:hover { color: var(--fg); text-decoration: underline; }
|
|
889
|
+
|
|
890
|
+
/* ─── Responsive ──────────────────────────────────────────────────── */
|
|
891
|
+
@media (max-width: 1023px) {
|
|
892
|
+
#mobile-header {
|
|
893
|
+
height: 56px; flex: 0 0 56px; display: flex; align-items: center; gap: 12px; padding: 0 14px;
|
|
894
|
+
border-bottom: 1px solid var(--border-soft); background: var(--surface);
|
|
895
|
+
}
|
|
896
|
+
#mobile-header b { font: 650 15px/1 var(--font-display); letter-spacing: -0.01em; color: var(--fg); }
|
|
897
|
+
#sidebar {
|
|
898
|
+
position: fixed; inset: 0 auto 0 0; width: min(292px, 84vw) !important; transform: translateX(-100%);
|
|
899
|
+
transition: transform 0.22s cubic-bezier(0.23, 1, 0.32, 1);
|
|
900
|
+
box-shadow: 20px 0 50px rgba(20, 20, 19, 0.18);
|
|
901
|
+
}
|
|
902
|
+
body.mobile-sidebar-open #sidebar { transform: translateX(0); }
|
|
903
|
+
body.sidebar-collapsed #brand, body.sidebar-collapsed .sidebar-copy { display: flex; }
|
|
904
|
+
body.sidebar-collapsed #brand-row { justify-content: space-between; padding: 0 16px; }
|
|
905
|
+
body.sidebar-collapsed #new-session, body.sidebar-collapsed .sess, body.sidebar-collapsed .side-action { justify-content: flex-start; padding-inline: 12px; margin-inline: 8px; }
|
|
906
|
+
body.sidebar-collapsed .sess span { display: block; }
|
|
907
|
+
body.sidebar-collapsed .sess::before { left: 5px; transform: translateY(-50%); }
|
|
908
|
+
body.sidebar-collapsed .sess.current::before { left: 5px; }
|
|
909
|
+
body.sidebar-collapsed .session-delete { display: grid; }
|
|
910
|
+
body.sidebar-collapsed #sidebar-footer { display: block; }
|
|
911
|
+
#sidebar-toggle { display: none; }
|
|
912
|
+
#mobile-close { display: grid; }
|
|
913
|
+
#mobile-mask { position: fixed; inset: 0; z-index: 25; border: 0; background: rgba(20, 20, 19, 0.35); }
|
|
914
|
+
body.mobile-sidebar-open #mobile-mask { display: block; }
|
|
915
|
+
#conv-header { padding: 10px 14px; }
|
|
916
|
+
#ws-chip { max-width: 30vw; }
|
|
917
|
+
#status-pill { display: none; }
|
|
918
|
+
#details.open { position: fixed; z-index: 45; inset: 0 0 0 auto; width: min(var(--details-width), 92vw); box-shadow: -20px 0 50px rgba(20, 20, 19, 0.18); }
|
|
919
|
+
#dock-stack { top: 12px; right: 12px; max-height: calc(100% - 24px); }
|
|
920
|
+
#activity-dock, #git-dock { width: 260px; }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
@media (max-width: 680px) {
|
|
924
|
+
#dock-stack { top: 10px; right: 10px; gap: 8px; max-width: calc(100% - 20px); align-items: stretch; }
|
|
925
|
+
#activity-dock, #git-dock { width: auto; max-width: none; }
|
|
926
|
+
#act-model span, #act-cwd span { max-width: 40vw; }
|
|
927
|
+
#conv-header { gap: 8px; padding: 8px 12px; }
|
|
928
|
+
#heading-wrap, #ws-chip { display: none; }
|
|
929
|
+
#model-chip { max-width: none; flex: 1; }
|
|
930
|
+
.view-tabs button { padding: 0 8px; }
|
|
931
|
+
.tab-chat-container { padding: 20px 16px; gap: 16px; }
|
|
932
|
+
#tab-bar { padding: 4px 8px 0; }
|
|
933
|
+
.tab-item { max-width: 140px; }
|
|
934
|
+
.row-user, .row-assistant, details.disclose { max-width: 96%; }
|
|
935
|
+
#composer-dock { padding: 0 12px 12px; }
|
|
936
|
+
#stats-line { gap: 10px; overflow-x: auto; white-space: nowrap; }
|
|
937
|
+
#stats-line span:nth-of-type(2), #stats-line span:nth-of-type(4) { display: none; }
|
|
938
|
+
#composer-hint { display: none; }
|
|
939
|
+
#input { min-height: 58px; }
|
|
940
|
+
#modal { grid-template-columns: 1fr; height: min(640px, calc(100dvh - 24px)); }
|
|
941
|
+
#modal nav { flex-direction: row; overflow-x: auto; padding: 8px; border-right: 0; border-bottom: 1px solid var(--border-soft); }
|
|
942
|
+
#modal nav::before { display: none; }
|
|
943
|
+
#modal nav button { flex: 1; text-align: center; }
|
|
944
|
+
#modal-body { padding: 20px 16px; }
|
|
945
|
+
}
|
|
946
|
+
</style>
|
|
947
|
+
</head>
|
|
948
|
+
<body>
|
|
949
|
+
<div id="app">
|
|
950
|
+
<button id="mobile-mask" aria-label="Close navigation"></button>
|
|
951
|
+
|
|
952
|
+
<aside id="sidebar" aria-label="Pi navigation">
|
|
953
|
+
<div id="brand-row">
|
|
954
|
+
<div id="brand">
|
|
955
|
+
<div id="brand-mark">π</div>
|
|
956
|
+
<div id="brand-name">Pi Agent</div>
|
|
957
|
+
</div>
|
|
958
|
+
<button id="sidebar-toggle" class="icon-button" title="Collapse navigation" aria-label="Collapse navigation">
|
|
959
|
+
<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16M14 9l-3 3 3 3"/></svg>
|
|
960
|
+
</button>
|
|
961
|
+
<button id="mobile-close" class="icon-button" aria-label="Close navigation">
|
|
962
|
+
<svg class="icon" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
|
963
|
+
</button>
|
|
964
|
+
</div>
|
|
965
|
+
|
|
966
|
+
<button id="new-session">
|
|
967
|
+
<svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
|
|
968
|
+
<span class="sidebar-copy">New session</span>
|
|
969
|
+
</button>
|
|
970
|
+
|
|
971
|
+
<div class="nav-section-label sidebar-copy" id="tabs-label" hidden>Tabs</div>
|
|
972
|
+
<div id="sidebar-tabs"></div>
|
|
973
|
+
|
|
974
|
+
<div class="nav-section-label sidebar-copy">Sessions</div>
|
|
975
|
+
<div id="session-list"></div>
|
|
976
|
+
|
|
977
|
+
<div id="side-system">
|
|
978
|
+
<div class="nav-section-label sidebar-copy">System</div>
|
|
979
|
+
<button id="settings-btn" class="side-action">
|
|
980
|
+
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.09A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3v-4h.09A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.09A1.7 1.7 0 0 0 15.4 4a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.2.37.55.72 1 .9.34.14.7.2 1.1.2H21v4h-.09a1.7 1.7 0 0 0-1.51.9z"/></svg>
|
|
981
|
+
<span class="sidebar-copy">Settings</span>
|
|
982
|
+
<span id="gateway-dot"></span>
|
|
983
|
+
</button>
|
|
984
|
+
</div>
|
|
985
|
+
<div id="sidebar-footer"><b>LOCAL RUNTIME</b><br /><span id="footer-status">gateway connected</span></div>
|
|
986
|
+
</aside>
|
|
987
|
+
|
|
988
|
+
<main id="main">
|
|
989
|
+
<div id="mobile-header">
|
|
990
|
+
<button id="mobile-open" class="icon-button" aria-label="Open navigation">
|
|
991
|
+
<svg class="icon" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
|
|
992
|
+
</button>
|
|
993
|
+
<b>Pi Agent</b>
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<header id="conv-header">
|
|
997
|
+
<div id="heading-wrap">
|
|
998
|
+
<div id="eyebrow">Active session</div>
|
|
999
|
+
<div id="conv-title">New session</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
<button id="ws-chip" title="Choose workspace">Loading workspace…</button>
|
|
1002
|
+
<button id="model-chip" title="Search and switch model"><span>Loading model…</span></button>
|
|
1003
|
+
<div class="view-tabs" role="tablist">
|
|
1004
|
+
<button data-view="chat" class="active" role="tab">Chat</button>
|
|
1005
|
+
<button data-view="traj" role="tab">Logs</button>
|
|
1006
|
+
</div>
|
|
1007
|
+
<button id="theme-toggle" class="icon-button" title="Theme: system" aria-label="Theme: system"></button>
|
|
1008
|
+
<div id="status-pill">Idle</div>
|
|
1009
|
+
</header>
|
|
1010
|
+
|
|
1011
|
+
<!-- Parallel session tabs -->
|
|
1012
|
+
<div id="tab-bar" role="tablist" aria-label="Parallel sessions">
|
|
1013
|
+
<button id="tab-add" type="button" title="New parallel session tab" aria-label="New parallel session tab">+</button>
|
|
1014
|
+
</div>
|
|
1015
|
+
|
|
1016
|
+
<div id="views">
|
|
1017
|
+
<section class="view active" id="view-chat" aria-label="Conversation"></section>
|
|
1018
|
+
<section class="view" id="view-traj" aria-label="Session logs"><div id="traj-body"></div></section>
|
|
1019
|
+
|
|
1020
|
+
<div id="dock-stack">
|
|
1021
|
+
<aside id="git-dock" aria-label="Git" hidden>
|
|
1022
|
+
<button type="button" id="git-toggle" aria-expanded="true" aria-controls="git-body">
|
|
1023
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="8" r="2.5"/><path d="M6 8.5v7M18 10.5c0 3.2-3.4 4-6.2 4.6"/></svg>
|
|
1024
|
+
<span class="act-title">Git</span>
|
|
1025
|
+
<span class="git-repo" id="git-repo"></span>
|
|
1026
|
+
<span class="act-chev" aria-hidden="true"><svg class="icon" viewBox="0 0 24 24"><path d="M6 14l6-6 6 6"/></svg></span>
|
|
1027
|
+
</button>
|
|
1028
|
+
<div id="git-body">
|
|
1029
|
+
<button type="button" class="git-row" id="git-branch-row" title="Switch branch">
|
|
1030
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="8" r="2.5"/><path d="M6 8.5v7M18 10.5c0 3.2-3.4 4-6.2 4.6"/></svg>
|
|
1031
|
+
<span class="git-row-label">Branch</span>
|
|
1032
|
+
<b id="git-branch">–</b>
|
|
1033
|
+
<span class="git-chev" aria-hidden="true"><svg class="icon" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg></span>
|
|
1034
|
+
</button>
|
|
1035
|
+
<button type="button" class="git-row" id="git-changes-row" title="Review changed files">
|
|
1036
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/></svg>
|
|
1037
|
+
<span class="git-row-label">Changes</span>
|
|
1038
|
+
<b id="git-adds">+0</b><b id="git-dels">−0</b>
|
|
1039
|
+
</button>
|
|
1040
|
+
<button type="button" class="git-row" id="git-sync-row" title="Push / pull against upstream">
|
|
1041
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M17 3l4 4-4 4M21 7H8M7 21l-4-4 4-4M3 17h13"/></svg>
|
|
1042
|
+
<span class="git-row-label">Sync</span>
|
|
1043
|
+
<b id="git-sync">–</b>
|
|
1044
|
+
</button>
|
|
1045
|
+
<button type="button" class="git-commit" id="git-commit-btn">Stage all & commit</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
</aside>
|
|
1048
|
+
|
|
1049
|
+
<aside id="activity-dock" aria-label="Session activity">
|
|
1050
|
+
<button type="button" id="activity-toggle" aria-expanded="true" aria-controls="activity-body">
|
|
1051
|
+
<span class="act-live" aria-hidden="true"></span>
|
|
1052
|
+
<span class="act-title">Session activity</span>
|
|
1053
|
+
<span class="act-count" id="act-count"></span>
|
|
1054
|
+
<span class="act-chev" aria-hidden="true"><svg class="icon" viewBox="0 0 24 24"><path d="M6 14l6-6 6 6"/></svg></span>
|
|
1055
|
+
</button>
|
|
1056
|
+
<div id="activity-body">
|
|
1057
|
+
<div class="act-sec">
|
|
1058
|
+
<div class="act-row" id="act-model"></div>
|
|
1059
|
+
<div class="act-row" id="act-cwd"></div>
|
|
1060
|
+
</div>
|
|
1061
|
+
<div class="act-sec">
|
|
1062
|
+
<div class="act-label">Task</div>
|
|
1063
|
+
<div id="act-task">—</div>
|
|
1064
|
+
<div id="act-stats">0 turns · 0 tokens · $0.0000</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div class="act-sec">
|
|
1067
|
+
<div class="act-label">Progress</div>
|
|
1068
|
+
<ol id="act-steps"></ol>
|
|
1069
|
+
<div id="act-empty">No tasks yet</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
</aside>
|
|
1073
|
+
</div>
|
|
1074
|
+
</div>
|
|
1075
|
+
|
|
1076
|
+
<div id="composer-dock">
|
|
1077
|
+
<div id="stats-line">
|
|
1078
|
+
<span>Turns <b id="st-turns">0</b></span>
|
|
1079
|
+
<span>Steps <b id="st-steps">0</b></span>
|
|
1080
|
+
<span>Tokens <b id="st-in">0</b> in / <b id="st-out">0</b> out</span>
|
|
1081
|
+
<span>Cache <b id="st-cache">–</b></span>
|
|
1082
|
+
<span>Cost <b id="st-cost">$0.0000</b></span>
|
|
1083
|
+
<div id="ctx-ring" title="Context usage unavailable">
|
|
1084
|
+
<span>CTX</span><b id="st-ctx">– / –</b><span class="context-bar"><i id="ctx-fill"></i></span><span id="st-ctx-pct">–</span>
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div id="input-shell">
|
|
1088
|
+
<div id="autocomplete" role="listbox" aria-label="Suggestions"></div>
|
|
1089
|
+
<textarea id="input" placeholder="Ask Pi… / commands # skills @ files" aria-label="Message Pi" aria-autocomplete="list" aria-controls="autocomplete"></textarea>
|
|
1090
|
+
<div id="composer-actions">
|
|
1091
|
+
<span id="composer-hint">ENTER TO SEND · SHIFT + ENTER FOR NEW LINE</span>
|
|
1092
|
+
<button class="btn secondary" id="steer-btn">Steer</button>
|
|
1093
|
+
<button class="btn secondary" id="abort">Stop</button>
|
|
1094
|
+
<button class="btn" id="send">Send</button>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
</main>
|
|
1099
|
+
|
|
1100
|
+
<aside id="details">
|
|
1101
|
+
<div id="details-head"><span>Tool Inspector</span><button id="details-close" aria-label="Close inspector">×</button></div>
|
|
1102
|
+
<div id="details-body"><span class="muted">Select a completed tool call to inspect its input and output.</span></div>
|
|
1103
|
+
</aside>
|
|
1104
|
+
</div>
|
|
1105
|
+
|
|
1106
|
+
<div id="mask"></div>
|
|
1107
|
+
<div id="modal" role="dialog" aria-modal="true" aria-label="Settings">
|
|
1108
|
+
<nav>
|
|
1109
|
+
<button data-sect="general" class="active">General</button>
|
|
1110
|
+
<button data-sect="models">Models</button>
|
|
1111
|
+
<button data-sect="skills">Skills</button>
|
|
1112
|
+
<button data-sect="config">Config</button>
|
|
1113
|
+
</nav>
|
|
1114
|
+
<div id="modal-body"></div>
|
|
1115
|
+
</div>
|
|
1116
|
+
|
|
1117
|
+
<script src="/vendor/marked.js"></script>
|
|
1118
|
+
<script src="/vendor/purify.js"></script>
|
|
1119
|
+
<script>
|
|
1120
|
+
const $ = (id) => document.getElementById(id);
|
|
1121
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g,
|
|
1122
|
+
(c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c]));
|
|
1123
|
+
const iconFolder = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7h7l2 2h9v10H3z"/></svg>`;
|
|
1124
|
+
|
|
1125
|
+
// ─── Parallel tab state ───────────────────────────────────────────────────
|
|
1126
|
+
// Each tab is an independent server-side session with its own SSE stream.
|
|
1127
|
+
// Per-tab: { id, name, chatEl, streamingEl, streamingText, thinkingEl,
|
|
1128
|
+
// runningFlag, currentSessionFile, toolInputs, activity, lastStats,
|
|
1129
|
+
// eventSource }
|
|
1130
|
+
const tabStates = new Map();
|
|
1131
|
+
let activeTabId = null;
|
|
1132
|
+
let running = false; // active tab's run state — drives the status pill
|
|
1133
|
+
let currentSessionFile = null; // active tab's session file — drives sidebar highlight
|
|
1134
|
+
let commandCatalog = { commands: [], skills: [] };
|
|
1135
|
+
const webBuiltinNames = new Set(["settings", "model", "new", "resume", "name", "session", "compact", "reload", "newtab"]);
|
|
1136
|
+
let completionItems = [], completionIndex = 0, completionToken = null;
|
|
1137
|
+
let completionRequest = 0;
|
|
1138
|
+
const chatView = $("view-chat");
|
|
1139
|
+
|
|
1140
|
+
function getActiveTab() { return tabStates.get(activeTabId); }
|
|
1141
|
+
|
|
1142
|
+
function createTabState(id, name) {
|
|
1143
|
+
const chatEl = document.createElement("div");
|
|
1144
|
+
chatEl.className = "tab-chat-container";
|
|
1145
|
+
chatEl.dataset.tabId = id;
|
|
1146
|
+
chatEl.style.display = "none";
|
|
1147
|
+
chatView.appendChild(chatEl);
|
|
1148
|
+
|
|
1149
|
+
const state = {
|
|
1150
|
+
id,
|
|
1151
|
+
name: name || "New session",
|
|
1152
|
+
chatEl,
|
|
1153
|
+
streamingEl: null,
|
|
1154
|
+
streamingText: "",
|
|
1155
|
+
thinkingEl: null,
|
|
1156
|
+
runningFlag: false,
|
|
1157
|
+
currentSessionFile: null,
|
|
1158
|
+
toolInputs: {},
|
|
1159
|
+
activity: { taskText: null, tools: [], tasks: [] },
|
|
1160
|
+
lastStats: null,
|
|
1161
|
+
eventSource: null,
|
|
1162
|
+
};
|
|
1163
|
+
tabStates.set(id, state);
|
|
1164
|
+
connectTabSSE(state);
|
|
1165
|
+
return state;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function connectTabSSE(state) {
|
|
1169
|
+
if (state.eventSource) {
|
|
1170
|
+
state.eventSource.close();
|
|
1171
|
+
state.eventSource = null;
|
|
1172
|
+
}
|
|
1173
|
+
const es = new EventSource(`/api/events?tab=${encodeURIComponent(state.id)}`);
|
|
1174
|
+
es.onmessage = (message) => {
|
|
1175
|
+
try { handleTabEvent(state, JSON.parse(message.data)); }
|
|
1176
|
+
catch (error) { console.error("Tab event error", state.id, error); }
|
|
1177
|
+
};
|
|
1178
|
+
es.onerror = () => {
|
|
1179
|
+
if (state.id === activeTabId) $("footer-status").textContent = "gateway reconnecting";
|
|
1180
|
+
};
|
|
1181
|
+
es.onopen = () => {
|
|
1182
|
+
if (state.id === activeTabId) $("footer-status").textContent = "gateway connected";
|
|
1183
|
+
};
|
|
1184
|
+
state.eventSource = es;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function destroyTabState(id) {
|
|
1188
|
+
const state = tabStates.get(id);
|
|
1189
|
+
if (!state) return;
|
|
1190
|
+
if (state.eventSource) state.eventSource.close();
|
|
1191
|
+
state.chatEl.remove();
|
|
1192
|
+
tabStates.delete(id);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function switchToTab(id) {
|
|
1196
|
+
if (!tabStates.has(id)) return;
|
|
1197
|
+
activeTabId = id;
|
|
1198
|
+
for (const [tid, state] of tabStates) {
|
|
1199
|
+
state.chatEl.style.display = tid === id ? "flex" : "none";
|
|
1200
|
+
}
|
|
1201
|
+
document.querySelectorAll("#sidebar-tabs .sidebar-tab").forEach((el) => {
|
|
1202
|
+
el.classList.toggle("active", el.dataset.tabId === id);
|
|
1203
|
+
});
|
|
1204
|
+
const state = getActiveTab();
|
|
1205
|
+
if (state) {
|
|
1206
|
+
$("conv-title").textContent = state.name || "New session";
|
|
1207
|
+
currentSessionFile = state.currentSessionFile;
|
|
1208
|
+
setStatus(state.runningFlag ? "running" : "idle");
|
|
1209
|
+
renderActivity();
|
|
1210
|
+
if (state.lastStats) setStats(state.lastStats);
|
|
1211
|
+
document.querySelectorAll("#session-list .sess").forEach((item) => {
|
|
1212
|
+
item.classList.toggle("current", item.title === currentSessionFile);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
scrollBottom();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function scrollBottom() {
|
|
1219
|
+
const tab = getActiveTab();
|
|
1220
|
+
if (tab) tab.chatEl.scrollTop = tab.chatEl.scrollHeight;
|
|
1221
|
+
}
|
|
1222
|
+
function elInTab(state, cls, text) {
|
|
1223
|
+
const node = document.createElement("div");
|
|
1224
|
+
node.className = cls;
|
|
1225
|
+
if (text != null) node.textContent = text;
|
|
1226
|
+
state.chatEl.appendChild(node);
|
|
1227
|
+
return node;
|
|
1228
|
+
}
|
|
1229
|
+
// Non-event callers (command notes, submit errors) always mean the active tab.
|
|
1230
|
+
function el(cls, text) { return elInTab(getActiveTab(), cls, text); }
|
|
1231
|
+
|
|
1232
|
+
const markdownSanitizeOptions = {
|
|
1233
|
+
ALLOWED_TAGS: [
|
|
1234
|
+
"p", "br", "strong", "em", "del", "code", "pre", "blockquote",
|
|
1235
|
+
"ul", "ol", "li", "h1", "h2", "h3", "h4", "h5", "h6", "a", "hr",
|
|
1236
|
+
"table", "thead", "tbody", "tr", "th", "td", "input",
|
|
1237
|
+
],
|
|
1238
|
+
ALLOWED_ATTR: ["href", "title", "class", "type", "checked", "disabled", "start"],
|
|
1239
|
+
ALLOW_DATA_ATTR: false,
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
function setAssistantMarkdown(node, source) {
|
|
1243
|
+
const markdown = String(source ?? "");
|
|
1244
|
+
if (!window.marked?.parse || !window.DOMPurify?.sanitize) {
|
|
1245
|
+
node.textContent = markdown;
|
|
1246
|
+
return node;
|
|
1247
|
+
}
|
|
1248
|
+
const parsed = window.marked.parse(markdown, { gfm: true, breaks: false });
|
|
1249
|
+
node.innerHTML = window.DOMPurify.sanitize(parsed, markdownSanitizeOptions);
|
|
1250
|
+
node.querySelectorAll("a[href]").forEach((link) => {
|
|
1251
|
+
link.target = "_blank";
|
|
1252
|
+
link.rel = "noopener noreferrer";
|
|
1253
|
+
});
|
|
1254
|
+
return node;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function assistantRowInTab(state, markdown = "") {
|
|
1258
|
+
return setAssistantMarkdown(elInTab(state, "row-assistant", ""), markdown);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function toolRowInTab(state, toolCallId, name, argsJson) {
|
|
1262
|
+
const node = document.createElement("details");
|
|
1263
|
+
node.className = "disclose";
|
|
1264
|
+
node.dataset.callId = toolCallId || "";
|
|
1265
|
+
const shortArgs = (argsJson ?? "").replace(/\s+/g, " ").slice(0, 120);
|
|
1266
|
+
node.innerHTML = `<summary><span class="chev">▶</span><span class="tool-name">${esc(name)}</span><span class="tool-args muted">${esc(shortArgs)}</span><span class="spin muted"></span></summary>`;
|
|
1267
|
+
const input = document.createElement("div");
|
|
1268
|
+
input.className = "body";
|
|
1269
|
+
input.textContent = "Input:\n" + (argsJson ?? "");
|
|
1270
|
+
node.appendChild(input);
|
|
1271
|
+
state.chatEl.appendChild(node);
|
|
1272
|
+
if (state.id === activeTabId) scrollBottom();
|
|
1273
|
+
return node;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function finishTool(node, output, isError, state) {
|
|
1277
|
+
const spin = node.querySelector(".spin");
|
|
1278
|
+
if (spin) spin.textContent = isError ? "FAILED" : "DONE";
|
|
1279
|
+
if (isError) node.classList.add("err");
|
|
1280
|
+
node.querySelectorAll(".body").forEach((body) => body.remove());
|
|
1281
|
+
const tabState = state ?? tabStates.get(node.closest(".tab-chat-container")?.dataset.tabId);
|
|
1282
|
+
const input = document.createElement("div");
|
|
1283
|
+
input.className = "body";
|
|
1284
|
+
input.textContent = "Input:\n" + JSON.stringify(tabState?.toolInputs[node.dataset.callId] ?? {}, null, 2);
|
|
1285
|
+
const result = document.createElement("div");
|
|
1286
|
+
result.className = "body";
|
|
1287
|
+
result.textContent = "Output:\n" + output;
|
|
1288
|
+
node.append(input, result);
|
|
1289
|
+
node.addEventListener("click", (event) => {
|
|
1290
|
+
if (event.target.closest("summary")) openDetails(node);
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function openDetails(node) {
|
|
1295
|
+
$("details").classList.add("open");
|
|
1296
|
+
const name = node.querySelector(".tool-name")?.textContent ?? "Tool";
|
|
1297
|
+
$("details-body").innerHTML = `<div style="color:var(--accent);margin-bottom:12px;text-transform:uppercase;letter-spacing:.08em;font-weight:600">${esc(name)}</div>` +
|
|
1298
|
+
[...node.querySelectorAll(".body")].map((body) => `<pre class="json">${esc(body.textContent)}</pre>`).join("");
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ─── Floating activity dock ──────────────────────────────────────────────
|
|
1302
|
+
// Floating activity dock (reflects the active tab)
|
|
1303
|
+
const iconCheck = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>`;
|
|
1304
|
+
const iconRun = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="7"/></svg>`;
|
|
1305
|
+
const iconPending = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/></svg>`;
|
|
1306
|
+
const iconModel = `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="5" width="14" height="14" rx="3"/><path d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"/></svg>`;
|
|
1307
|
+
|
|
1308
|
+
// Extract tasks from assistant text — recognizes numbered lists, markdown checkboxes, Phase/Step headers
|
|
1309
|
+
function extractTasks(text) {
|
|
1310
|
+
if (!text || typeof text !== "string") return [];
|
|
1311
|
+
const tasks = [];
|
|
1312
|
+
const lines = text.split("\n");
|
|
1313
|
+
for (const line of lines) {
|
|
1314
|
+
const trimmed = line.trim();
|
|
1315
|
+
// Markdown task items: - [ ] task or - [x] task or * [x] task
|
|
1316
|
+
const mdTask = trimmed.match(/^[-*]\s*\[([ xX])\]\s+(.+)/);
|
|
1317
|
+
if (mdTask) {
|
|
1318
|
+
tasks.push({ text: mdTask[2].trim(), done: mdTask[1] !== " " });
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
// "T1:", "T2:" style — possibly preceded by bullet (• T1: ..., - T1: ..., * T1: ...)
|
|
1322
|
+
const tStyle = trimmed.match(/^(?:[-*\u2022]\s*)?T\d+[.:]\s+(.{8,})/);
|
|
1323
|
+
if (tStyle) {
|
|
1324
|
+
const taskText = tStyle[1].replace(/\*\*/g, "").replace(/`([^`]*)`/g, "$1").trim();
|
|
1325
|
+
if (taskText.length <= 150) {
|
|
1326
|
+
tasks.push({ text: taskText, done: false });
|
|
1327
|
+
}
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
// Phase/Step headers: "Phase 1 — ...", "Step 1: ...", "Task 1: ..."
|
|
1331
|
+
const phase = trimmed.match(/^(?:#{1,4}\s*)?(?:Phase|Step|Task)\s+\d+\s*[:\u2014\-\u2013\u2014]\s*(.+)/i);
|
|
1332
|
+
if (phase) {
|
|
1333
|
+
tasks.push({ text: phase[1].replace(/\*\*/g, "").trim(), done: false });
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
// Numbered list items: 1. task, 1) task — only if 8+ chars, not code, not too long
|
|
1337
|
+
const numbered = trimmed.match(/^\d+[.)]\s+(.{8,})/);
|
|
1338
|
+
if (numbered && !numbered[1].startsWith("`") && !numbered[1].startsWith("//") && !numbered[1].startsWith("```")) {
|
|
1339
|
+
const taskText = numbered[1].replace(/\*\*/g, "").replace(/`([^`]*)`/g, "$1").trim();
|
|
1340
|
+
if (taskText.length <= 120 && taskText.length >= 10) {
|
|
1341
|
+
tasks.push({ text: taskText, done: false });
|
|
1342
|
+
}
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// Deduplicate and cap at 30 tasks
|
|
1347
|
+
const seen = new Set();
|
|
1348
|
+
return tasks.filter((t) => {
|
|
1349
|
+
const key = t.text.toLowerCase().slice(0, 40);
|
|
1350
|
+
if (seen.has(key)) return false;
|
|
1351
|
+
seen.add(key);
|
|
1352
|
+
return true;
|
|
1353
|
+
}).slice(0, 30);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Check if assistant text indicates task completions
|
|
1357
|
+
function updateTaskCompletions(state, text) {
|
|
1358
|
+
if (!text || !state.activity.tasks.length) return false;
|
|
1359
|
+
let changed = false;
|
|
1360
|
+
const lower = text.toLowerCase();
|
|
1361
|
+
const completionSignals = ["done", "completed", "fixed", "finished", "resolved", "implemented", "added", "created", "\u2713", "\u2714", "\u2705"];
|
|
1362
|
+
for (const task of state.activity.tasks) {
|
|
1363
|
+
if (task.done) continue;
|
|
1364
|
+
// Check if the text references this task being completed
|
|
1365
|
+
const taskWords = task.text.toLowerCase().split(/\s+/).filter((w) => w.length > 3).slice(0, 4);
|
|
1366
|
+
if (taskWords.length === 0) continue;
|
|
1367
|
+
const taskMentioned = taskWords.filter((word) => lower.includes(word)).length >= 2;
|
|
1368
|
+
if (taskMentioned && completionSignals.some((signal) => lower.includes(signal))) {
|
|
1369
|
+
task.done = true;
|
|
1370
|
+
changed = true;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return changed;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function renderActivity() {
|
|
1377
|
+
const act = getActiveTab()?.activity ?? { taskText: null, tools: [], tasks: [] };
|
|
1378
|
+
const tasks = act.tasks;
|
|
1379
|
+
const hasTasks = tasks.length > 0;
|
|
1380
|
+
|
|
1381
|
+
if (hasTasks) {
|
|
1382
|
+
$("act-steps").innerHTML = tasks.map((task) => {
|
|
1383
|
+
const cls = task.done ? "done" : "pending";
|
|
1384
|
+
const mark = task.done ? iconCheck : iconPending;
|
|
1385
|
+
return `<li class="${cls}"><span class="act-check">${mark}</span><span class="act-name" title="${esc(task.text)}">${esc(task.text)}</span></li>`;
|
|
1386
|
+
}).join("");
|
|
1387
|
+
$("act-empty").style.display = "none";
|
|
1388
|
+
const doneCount = tasks.filter((t) => t.done).length;
|
|
1389
|
+
$("act-count").textContent = `${doneCount}/${tasks.length}`;
|
|
1390
|
+
} else {
|
|
1391
|
+
// Fall back to showing tool calls if no tasks extracted
|
|
1392
|
+
const steps = act.tools.slice(-6);
|
|
1393
|
+
$("act-steps").innerHTML = steps.map((tool) => {
|
|
1394
|
+
const cls = tool.state === "running" ? "run" : tool.state === "error" ? "err" : "done";
|
|
1395
|
+
const mark = tool.state === "running" ? iconRun : iconCheck;
|
|
1396
|
+
return `<li class="${cls}"><span class="act-check">${mark}</span><span class="act-name" title="${esc(tool.name)}">${esc(tool.name)}</span></li>`;
|
|
1397
|
+
}).join("");
|
|
1398
|
+
$("act-empty").style.display = act.tools.length ? "none" : "";
|
|
1399
|
+
const doneCount = act.tools.filter((tool) => tool.state === "done").length;
|
|
1400
|
+
$("act-count").textContent = act.tools.length ? `${doneCount}/${act.tools.length}` : "";
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
$("activity-dock").classList.toggle("live", running || act.tools.some((tool) => tool.state === "running"));
|
|
1404
|
+
if (act.taskText != null) $("act-task").textContent = act.taskText;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function setActivityTask(state, text) {
|
|
1408
|
+
const trimmed = String(text ?? "").trim();
|
|
1409
|
+
if (!trimmed) return;
|
|
1410
|
+
state.activity.taskText = trimmed.length > 300 ? trimmed.slice(0, 297) + "\u2026" : trimmed;
|
|
1411
|
+
if (state.id === activeTabId) renderActivity();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Called when the assistant produces a message — extract tasks or mark completions
|
|
1415
|
+
function processAssistantForTasks(state, text) {
|
|
1416
|
+
if (!text || typeof text !== "string") return;
|
|
1417
|
+
const newTasks = extractTasks(text);
|
|
1418
|
+
if (newTasks.length >= 3 && newTasks.length > state.activity.tasks.length) {
|
|
1419
|
+
// Agent produced a larger plan — replace the task list
|
|
1420
|
+
state.activity.tasks = newTasks;
|
|
1421
|
+
if (state.id === activeTabId) renderActivity();
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
// Otherwise check if this message indicates task completions
|
|
1425
|
+
if (updateTaskCompletions(state, text)) {
|
|
1426
|
+
if (state.id === activeTabId) renderActivity();
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const ACT_STORE = "pi:activity-dock:minimized";
|
|
1431
|
+
function setActivityMinimized(minimized) {
|
|
1432
|
+
$("activity-dock").classList.toggle("minimized", minimized);
|
|
1433
|
+
const toggle = $("activity-toggle");
|
|
1434
|
+
toggle.setAttribute("aria-expanded", String(!minimized));
|
|
1435
|
+
toggle.title = minimized ? "Expand session activity" : "Minimize session activity";
|
|
1436
|
+
try { localStorage.setItem(ACT_STORE, minimized ? "1" : "0"); } catch (_) {}
|
|
1437
|
+
}
|
|
1438
|
+
$("activity-toggle").addEventListener("click", () => {
|
|
1439
|
+
setActivityMinimized(!$("activity-dock").classList.contains("minimized"));
|
|
1440
|
+
});
|
|
1441
|
+
try { setActivityMinimized(localStorage.getItem(ACT_STORE) === "1"); } catch (_) {}
|
|
1442
|
+
$("details-close").onclick = () => $("details").classList.remove("open");
|
|
1443
|
+
|
|
1444
|
+
// ─── Git tile — branch, working-tree changes, commit ──────────────────────
|
|
1445
|
+
// Backed by /api/git/* endpoints; hides itself when they are missing or the
|
|
1446
|
+
// workspace is not a git repository.
|
|
1447
|
+
const GIT_STORE = "pi:git-dock:minimized";
|
|
1448
|
+
function setGitMinimized(minimized) {
|
|
1449
|
+
$("git-dock").classList.toggle("minimized", minimized);
|
|
1450
|
+
const toggle = $("git-toggle");
|
|
1451
|
+
toggle.setAttribute("aria-expanded", String(!minimized));
|
|
1452
|
+
toggle.title = minimized ? "Expand Git panel" : "Minimize Git panel";
|
|
1453
|
+
try { localStorage.setItem(GIT_STORE, minimized ? "1" : "0"); } catch (_) {}
|
|
1454
|
+
}
|
|
1455
|
+
$("git-toggle").addEventListener("click", () => {
|
|
1456
|
+
setGitMinimized(!$("git-dock").classList.contains("minimized"));
|
|
1457
|
+
});
|
|
1458
|
+
try { setGitMinimized(localStorage.getItem(GIT_STORE) === "1"); } catch (_) {}
|
|
1459
|
+
|
|
1460
|
+
let lastGitStatus = null;
|
|
1461
|
+
async function loadGitStatus() {
|
|
1462
|
+
try {
|
|
1463
|
+
const res = await fetch("/api/git/status");
|
|
1464
|
+
if (!res.ok) throw new Error("unavailable");
|
|
1465
|
+
const data = await res.json();
|
|
1466
|
+
if (!data.ok || !data.isRepo) { $("git-dock").hidden = true; return; }
|
|
1467
|
+
lastGitStatus = data;
|
|
1468
|
+
$("git-dock").hidden = false;
|
|
1469
|
+
$("git-repo").textContent = data.repo ?? "";
|
|
1470
|
+
$("git-branch").textContent = data.branch || "HEAD";
|
|
1471
|
+
const count = (data.changes ?? []).length;
|
|
1472
|
+
$("git-adds").textContent = "+" + (data.totalAdditions ?? 0).toLocaleString();
|
|
1473
|
+
$("git-dels").textContent = "−" + (data.totalDeletions ?? 0).toLocaleString();
|
|
1474
|
+
$("git-changes-row").title = count
|
|
1475
|
+
? `${count} changed file${count === 1 ? "" : "s"}`
|
|
1476
|
+
: "Working tree clean";
|
|
1477
|
+
$("git-commit-btn").disabled = count === 0;
|
|
1478
|
+
const hasUpstream = data.ahead != null || data.behind != null;
|
|
1479
|
+
$("git-sync").textContent = hasUpstream ? `↑${data.ahead ?? 0} ↓${data.behind ?? 0}` : "–";
|
|
1480
|
+
$("git-sync-row").title = hasUpstream
|
|
1481
|
+
? `${data.ahead ?? 0} ahead · ${data.behind ?? 0} behind upstream — push/pull`
|
|
1482
|
+
: "No upstream configured";
|
|
1483
|
+
} catch {
|
|
1484
|
+
$("git-dock").hidden = true;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Unified-diff renderer: lines → colored rows (2.5.4)
|
|
1489
|
+
function renderDiff(diffText) {
|
|
1490
|
+
return String(diffText ?? "")
|
|
1491
|
+
.split("\n")
|
|
1492
|
+
.map((line) => {
|
|
1493
|
+
const cls = line.startsWith("+") && !line.startsWith("+++") ? "add"
|
|
1494
|
+
: line.startsWith("-") && !line.startsWith("---") ? "del"
|
|
1495
|
+
: line.startsWith("@@") ? "hunk" : "";
|
|
1496
|
+
return `<span class="dl ${cls}">${esc(line || " ")}</span>`;
|
|
1497
|
+
})
|
|
1498
|
+
.join("");
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
async function openFileDiff(path) {
|
|
1502
|
+
const res = await fetch("/api/git/diff?file=" + encodeURIComponent(path));
|
|
1503
|
+
const data = await res.json().catch(() => ({}));
|
|
1504
|
+
if (!res.ok || !data.ok) { alert("Diff unavailable: " + (data.error ?? "unknown")); return; }
|
|
1505
|
+
modalOpen(`<h2 class="sect">Diff — <span class="mono">${esc(path)}</span></h2><p><button class="btn secondary" id="diff-back">← All changes</button></p><div class="diff-view">${renderDiff(data.diff) || '<span class="muted">No textual changes (binary or mode-only).</span>'}</div>`);
|
|
1506
|
+
$("diff-back").onclick = openGitChanges;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function openCommitDiff(hash) {
|
|
1510
|
+
const res = await fetch("/api/git/commit-diff?hash=" + encodeURIComponent(hash));
|
|
1511
|
+
const data = await res.json().catch(() => ({}));
|
|
1512
|
+
if (!res.ok || !data.ok) { alert("Diff unavailable: " + (data.error ?? "unknown")); return; }
|
|
1513
|
+
modalOpen(`<h2 class="sect">Commit <span class="mono">${esc(hash)}</span></h2><div class="diff-view">${renderDiff(data.diff)}</div>`);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Selective staging state for the changes modal
|
|
1517
|
+
let stagingSelection = new Set();
|
|
1518
|
+
let stagingKnown = new Set();
|
|
1519
|
+
let gitModalSelectedPaths = null;
|
|
1520
|
+
|
|
1521
|
+
async function openGitBranches() {
|
|
1522
|
+
openGitModal("branches");
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
async function openGitChanges() {
|
|
1526
|
+
openGitModal("changes");
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function openGitCommit(selectedPaths = null) {
|
|
1530
|
+
openGitModal("commit", selectedPaths);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async function openGitSync() {
|
|
1534
|
+
openGitModal("sync");
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function openGitModal(activeTab, selectedPaths = null) {
|
|
1538
|
+
gitModalSelectedPaths = selectedPaths;
|
|
1539
|
+
const tabs = [
|
|
1540
|
+
{ id: "branches", label: "Branches" },
|
|
1541
|
+
{ id: "changes", label: "Changes" },
|
|
1542
|
+
{ id: "commit", label: "Commit" },
|
|
1543
|
+
{ id: "sync", label: "Sync" },
|
|
1544
|
+
];
|
|
1545
|
+
const tabBar = `<div class="git-tabs">${tabs.map((t) =>
|
|
1546
|
+
`<button data-git-tab="${t.id}" class="${t.id === activeTab ? "active" : ""}">${t.label}</button>`
|
|
1547
|
+
).join("")}</div>`;
|
|
1548
|
+
modalOpen(`${tabBar}<div id="git-tab-content"></div>`);
|
|
1549
|
+
document.querySelectorAll(".git-tabs button").forEach((btn) => {
|
|
1550
|
+
btn.onclick = () => {
|
|
1551
|
+
document.querySelectorAll(".git-tabs button").forEach((b) => b.classList.remove("active"));
|
|
1552
|
+
btn.classList.add("active");
|
|
1553
|
+
loadGitTab(btn.dataset.gitTab);
|
|
1554
|
+
};
|
|
1555
|
+
});
|
|
1556
|
+
loadGitTab(activeTab);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
async function loadGitTab(tab) {
|
|
1560
|
+
const container = $("git-tab-content");
|
|
1561
|
+
if (!container) return;
|
|
1562
|
+
container.innerHTML = '<span class="muted">Loading…</span>';
|
|
1563
|
+
switch (tab) {
|
|
1564
|
+
case "branches": await renderGitBranches(container); break;
|
|
1565
|
+
case "changes": await renderGitChanges(container); break;
|
|
1566
|
+
case "commit": renderGitCommitContent(container, gitModalSelectedPaths); break;
|
|
1567
|
+
case "sync": renderGitSyncContent(container); break;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function renderGitBranches(container) {
|
|
1572
|
+
let data;
|
|
1573
|
+
try {
|
|
1574
|
+
const res = await fetch("/api/git/branches");
|
|
1575
|
+
if (!res.ok) throw new Error("unavailable");
|
|
1576
|
+
data = await res.json();
|
|
1577
|
+
if (!data.ok) throw new Error("unavailable");
|
|
1578
|
+
} catch { container.innerHTML = '<span class="muted">Branch list unavailable.</span>'; return; }
|
|
1579
|
+
const rows = data.branches.map((branch) =>
|
|
1580
|
+
`<tr class="${branch.current ? "current" : "clickable"}" data-branch="${esc(branch.name)}"><td>${branch.current ? "\u2713 " : ""}${esc(branch.name)}</td><td style="text-align:right;white-space:nowrap">${branch.current ? '<span class="muted">current</span>' : `<button type="button" class="branch-delete" data-del="${esc(branch.name)}" title="Delete merged branch">Delete</button>`}</td></tr>`
|
|
1581
|
+
).join("");
|
|
1582
|
+
container.innerHTML = `<h2 class="sect">Branches</h2><p class="muted">Checkout, create, or delete merged branches in <span class="mono">${esc(lastGitStatus?.repo ?? "this repository")}</span>.</p><table><tbody>${rows || '<tr><td class="muted">No local branches</td></tr>'}</tbody></table><div class="inline-form"><input id="new-branch-name" placeholder="new-branch-name" autocomplete="off"><button class="btn" id="create-branch">Create</button></div>`;
|
|
1583
|
+
container.onclick = async (event) => {
|
|
1584
|
+
const del = event.target.closest("[data-del]");
|
|
1585
|
+
if (del) {
|
|
1586
|
+
const name = del.dataset.del;
|
|
1587
|
+
if (!window.confirm(`Delete merged branch "${name}"?`)) return;
|
|
1588
|
+
const response = await fetch("/api/git/branches/delete", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name }) });
|
|
1589
|
+
if (!response.ok) { alert("Delete failed: " + ((await response.json().catch(() => ({}))).error ?? "unknown")); return; }
|
|
1590
|
+
await renderGitBranches(container);
|
|
1591
|
+
loadGitStatus();
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const row = event.target.closest("tr[data-branch]");
|
|
1595
|
+
if (!row || row.classList.contains("current")) return;
|
|
1596
|
+
const response = await fetch("/api/git/checkout", {
|
|
1597
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
1598
|
+
body: JSON.stringify({ branch: row.dataset.branch }),
|
|
1599
|
+
});
|
|
1600
|
+
if (!response.ok) {
|
|
1601
|
+
alert("Checkout failed: " + ((await response.json().catch(() => ({}))).error ?? "unknown"));
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
await loadGitStatus();
|
|
1605
|
+
await renderGitBranches(container);
|
|
1606
|
+
};
|
|
1607
|
+
const create = async () => {
|
|
1608
|
+
const name = $("new-branch-name").value.trim();
|
|
1609
|
+
if (!name) { $("new-branch-name").focus(); return; }
|
|
1610
|
+
const response = await fetch("/api/git/branches/create", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name, checkout: true }) });
|
|
1611
|
+
if (!response.ok) { alert("Create failed: " + ((await response.json().catch(() => ({}))).error ?? "unknown")); return; }
|
|
1612
|
+
await loadGitStatus();
|
|
1613
|
+
await renderGitBranches(container);
|
|
1614
|
+
};
|
|
1615
|
+
$("create-branch").onclick = create;
|
|
1616
|
+
$("new-branch-name").onkeydown = (event) => { if (event.key === "Enter") create(); };
|
|
1617
|
+
$("new-branch-name").focus();
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
async function renderGitChanges(container) {
|
|
1621
|
+
const status = lastGitStatus;
|
|
1622
|
+
if (!status) { container.innerHTML = '<span class="muted">No git status available.</span>'; return; }
|
|
1623
|
+
for (const change of status.changes) {
|
|
1624
|
+
if (!stagingKnown.has(change.path)) {
|
|
1625
|
+
stagingKnown.add(change.path);
|
|
1626
|
+
stagingSelection.add(change.path);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
for (const path of [...stagingKnown]) {
|
|
1630
|
+
if (!status.changes.some((change) => change.path === path)) {
|
|
1631
|
+
stagingKnown.delete(path);
|
|
1632
|
+
stagingSelection.delete(path);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const rows = status.changes.map((change) => {
|
|
1636
|
+
const staged = stagingSelection.has(change.path);
|
|
1637
|
+
return `<tr data-path="${esc(change.path)}"><td><input type="checkbox" class="stage-cb" data-stage="${esc(change.path)}" ${staged ? "checked" : ""} aria-label="Stage ${esc(change.path)}"></td><td class="mono commit-subject">${esc(change.path)}</td><td class="muted">${esc(change.status)}</td><td style="white-space:nowrap;text-align:right"><span class="diffstat-add">+${(change.additions ?? 0).toLocaleString()}</span> <span class="diffstat-del">\u2212${(change.deletions ?? 0).toLocaleString()}</span> <button type="button" class="file-diff-btn" data-diff="${esc(change.path)}">Diff</button></td></tr>`;
|
|
1638
|
+
}).join("");
|
|
1639
|
+
container.innerHTML = `<h2 class="sect">Working tree changes</h2><p class="muted">${status.changes.length} changed file${status.changes.length === 1 ? "" : "s"} on <b>${esc(status.branch)}</b> \u00b7 tick what to include, then commit.</p><table><thead><tr><th style="width:28px"></th><th>File</th><th>Status</th><th style="text-align:right">Lines</th></tr></thead><tbody id="changes-table-body">${rows || '<tr><td colspan="4" class="muted">Working tree clean</td></tr>'}</tbody></table><p style="margin:14px 0 0"><button class="btn" id="commit-selected">Commit selected</button></p><h2 class="sect">Recent commits</h2><div id="commit-history"><span class="muted">Loading\u2026</span></div>`;
|
|
1640
|
+
container.onchange = (event) => {
|
|
1641
|
+
const cb = event.target.closest("[data-stage]");
|
|
1642
|
+
if (!cb) return;
|
|
1643
|
+
if (cb.checked) stagingSelection.add(cb.dataset.stage);
|
|
1644
|
+
else stagingSelection.delete(cb.dataset.stage);
|
|
1645
|
+
};
|
|
1646
|
+
container.onclick = (event) => {
|
|
1647
|
+
const diff = event.target.closest("[data-diff]");
|
|
1648
|
+
if (diff) { openFileDiff(diff.dataset.diff); return; }
|
|
1649
|
+
};
|
|
1650
|
+
$("commit-selected").onclick = async () => {
|
|
1651
|
+
const selected = status.changes.filter((change) => stagingSelection.has(change.path)).map((change) => change.path);
|
|
1652
|
+
if (!selected.length) { alert("Select at least one file to commit."); return; }
|
|
1653
|
+
gitModalSelectedPaths = selected;
|
|
1654
|
+
document.querySelectorAll(".git-tabs button").forEach((b) => b.classList.toggle("active", b.dataset.gitTab === "commit"));
|
|
1655
|
+
loadGitTab("commit");
|
|
1656
|
+
};
|
|
1657
|
+
// History strip
|
|
1658
|
+
try {
|
|
1659
|
+
const logRes = await (await fetch("/api/git/log?limit=12")).json();
|
|
1660
|
+
const history = $("commit-history");
|
|
1661
|
+
if (!history) return;
|
|
1662
|
+
history.innerHTML = logRes.ok && logRes.commits.length
|
|
1663
|
+
? `<table><tbody>${logRes.commits.map((commit) => `<tr class="clickable" data-commit="${esc(commit.hash)}"><td class="mono">${esc(commit.hash)}</td><td class="commit-subject">${esc(commit.subject)}</td><td class="muted" style="white-space:nowrap">${esc(commit.when)}</td></tr>`).join("")}</tbody></table>`
|
|
1664
|
+
: '<span class="muted">No commits yet.</span>';
|
|
1665
|
+
history.onclick = (event) => {
|
|
1666
|
+
const row = event.target.closest("[data-commit]");
|
|
1667
|
+
if (row) openCommitDiff(row.dataset.commit);
|
|
1668
|
+
};
|
|
1669
|
+
} catch { const history = $("commit-history"); if (history) history.innerHTML = '<span class="muted">History unavailable.</span>'; }
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function renderGitCommitContent(container, selectedPaths) {
|
|
1673
|
+
const status = lastGitStatus;
|
|
1674
|
+
const selective = Array.isArray(selectedPaths);
|
|
1675
|
+
const count = selective ? selectedPaths.length : (status?.changes.length ?? 0);
|
|
1676
|
+
container.innerHTML = `<h2 class="sect">Commit changes</h2><p class="muted">${selective ? `Stages the ${count} selected file${count === 1 ? "" : "s"}` : `Stages all changes (${count} file${count === 1 ? "" : "s"})`} and commits on <b>${esc(status?.branch ?? "")}</b>.</p><textarea id="commit-msg" class="modal-textarea" placeholder="Commit message\u2026" maxlength="500"></textarea><button class="btn" id="commit-confirm">${selective ? `Commit ${count} file${count === 1 ? "" : "s"}` : "Stage all & commit"}</button>`;
|
|
1677
|
+
$("commit-msg").focus();
|
|
1678
|
+
$("commit-confirm").onclick = async () => {
|
|
1679
|
+
const message = $("commit-msg").value.trim();
|
|
1680
|
+
if (!message) { $("commit-msg").focus(); return; }
|
|
1681
|
+
const button = $("commit-confirm");
|
|
1682
|
+
button.disabled = true;
|
|
1683
|
+
button.textContent = "Committing\u2026";
|
|
1684
|
+
const payload = { message, stageAll: !selective };
|
|
1685
|
+
if (selective) {
|
|
1686
|
+
payload.stage = selectedPaths;
|
|
1687
|
+
payload.unstage = (lastGitStatus?.changes ?? []).filter((change) => !selectedPaths.includes(change.path)).map((change) => change.path);
|
|
1688
|
+
}
|
|
1689
|
+
if (selective) {
|
|
1690
|
+
const stageRes = await fetch("/api/git/stage", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ files: payload.stage, action: "stage" }) });
|
|
1691
|
+
if (!stageRes.ok) { button.disabled = false; button.textContent = "Commit"; alert("Staging failed: " + ((await stageRes.json().catch(() => ({}))).error ?? "unknown")); return; }
|
|
1692
|
+
if (payload.unstage.length) {
|
|
1693
|
+
await fetch("/api/git/stage", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ files: payload.unstage, action: "unstage" }) });
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const response = await fetch("/api/git/commit", {
|
|
1697
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
1698
|
+
body: JSON.stringify({ message, stageAll: !selective }),
|
|
1699
|
+
});
|
|
1700
|
+
if (!response.ok) {
|
|
1701
|
+
button.disabled = false;
|
|
1702
|
+
button.textContent = selective ? "Commit" : "Stage all & commit";
|
|
1703
|
+
alert("Commit failed: " + ((await response.json().catch(() => ({}))).error ?? "unknown"));
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
stagingSelection = new Set();
|
|
1707
|
+
stagingKnown = new Set();
|
|
1708
|
+
gitModalSelectedPaths = null;
|
|
1709
|
+
closeModal();
|
|
1710
|
+
loadGitStatus();
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function renderGitSyncContent(container) {
|
|
1715
|
+
const status = lastGitStatus;
|
|
1716
|
+
if (!status) { container.innerHTML = '<span class="muted">No git status available.</span>'; return; }
|
|
1717
|
+
const hasUpstream = status.ahead != null || status.behind != null;
|
|
1718
|
+
container.innerHTML = `<h2 class="sect">Sync \u2014 <b>${esc(status.branch)}</b></h2><p class="muted">${hasUpstream ? `${status.ahead ?? 0} commit${(status.ahead ?? 0) === 1 ? "" : "s"} ahead \u00b7 ${status.behind ?? 0} behind upstream` : "No upstream configured for this branch."}</p><p style="display:flex;gap:8px"><button class="btn" id="git-push">Push</button><button class="btn secondary" id="git-pull">Pull</button></p><pre class="sync-output" id="sync-output" hidden>Push or pull to see output.</pre>`;
|
|
1719
|
+
const run = (action) => async () => {
|
|
1720
|
+
const out = $("sync-output");
|
|
1721
|
+
out.hidden = false;
|
|
1722
|
+
out.textContent = `${action}ing\u2026`;
|
|
1723
|
+
const response = await fetch("/api/git/sync", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action }) });
|
|
1724
|
+
const data = await response.json().catch(() => ({}));
|
|
1725
|
+
if (!response.ok || !data.ok) {
|
|
1726
|
+
out.textContent = `\u2717 ${(data.error ?? "failed")}`;
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
out.textContent = data.output || `${action} complete.`;
|
|
1730
|
+
await loadGitStatus();
|
|
1731
|
+
};
|
|
1732
|
+
$("git-push").onclick = run("push");
|
|
1733
|
+
$("git-pull").onclick = run("pull");
|
|
1734
|
+
}
|
|
1735
|
+
$("git-branch-row").onclick = openGitBranches;
|
|
1736
|
+
$("git-changes-row").onclick = openGitChanges;
|
|
1737
|
+
$("git-commit-btn").onclick = () => openGitCommit();
|
|
1738
|
+
$("git-sync-row").onclick = openGitSync;
|
|
1739
|
+
loadGitStatus();
|
|
1740
|
+
|
|
1741
|
+
function setStatus(status) {
|
|
1742
|
+
const tab = getActiveTab();
|
|
1743
|
+
if (tab) tab.runningFlag = status === "running" || status === "retrying";
|
|
1744
|
+
running = tab?.runningFlag ?? false;
|
|
1745
|
+
document.body.classList.toggle("running", running);
|
|
1746
|
+
const pill = $("status-pill");
|
|
1747
|
+
pill.textContent = status;
|
|
1748
|
+
pill.className = running ? "running" : "";
|
|
1749
|
+
$("footer-status").textContent = running ? "agent processing" : "gateway connected";
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function formatTokenCount(value) {
|
|
1753
|
+
if (value == null || !Number.isFinite(Number(value))) return "–";
|
|
1754
|
+
const number = Number(value);
|
|
1755
|
+
if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(number % 1_000_000 ? 1 : 0)}m`;
|
|
1756
|
+
if (number >= 1_000) return `${(number / 1_000).toFixed(number % 1_000 ? 1 : 0)}k`;
|
|
1757
|
+
return number.toLocaleString();
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function setStats(stats) {
|
|
1761
|
+
$("act-stats").textContent = `${stats.turns ?? 0} turns · ${formatTokenCount((stats.input ?? 0) + (stats.output ?? 0))} tokens · $${(+(stats.cost ?? 0)).toFixed(4)}`;
|
|
1762
|
+
$("st-turns").textContent = stats.turns ?? 0;
|
|
1763
|
+
$("st-steps").textContent = stats.steps ?? 0;
|
|
1764
|
+
$("st-in").textContent = (stats.input ?? 0).toLocaleString();
|
|
1765
|
+
$("st-out").textContent = (stats.output ?? 0).toLocaleString();
|
|
1766
|
+
$("st-cache").textContent = stats.cachePct > 0 ? stats.cachePct + "%" : "–";
|
|
1767
|
+
$("st-cost").textContent = "$" + (+(stats.cost ?? 0)).toFixed(4);
|
|
1768
|
+
|
|
1769
|
+
const context = $("ctx-ring");
|
|
1770
|
+
const limit = Number(stats.contextWindow) || 0;
|
|
1771
|
+
const used = stats.contextTokens == null ? null : Number(stats.contextTokens);
|
|
1772
|
+
const percent = stats.contextPercent == null
|
|
1773
|
+
? (used == null || !limit ? null : (used / limit) * 100)
|
|
1774
|
+
: Number(stats.contextPercent);
|
|
1775
|
+
context.classList.remove("warning", "danger");
|
|
1776
|
+
if (!limit) {
|
|
1777
|
+
$("st-ctx").textContent = "– / –";
|
|
1778
|
+
$("st-ctx-pct").textContent = "–";
|
|
1779
|
+
$("ctx-fill").style.width = "0%";
|
|
1780
|
+
context.title = "Context usage unavailable";
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
$("st-ctx").textContent = `${used == null ? "estimating" : formatTokenCount(used)} / ${formatTokenCount(limit)}`;
|
|
1785
|
+
$("st-ctx-pct").textContent = percent == null ? "–" : `${percent.toFixed(percent < 10 ? 1 : 0)}%`;
|
|
1786
|
+
$("ctx-fill").style.width = `${Math.min(100, Math.max(0, percent ?? 0))}%`;
|
|
1787
|
+
if (percent >= 90) context.classList.add("danger");
|
|
1788
|
+
else if (percent >= 70) context.classList.add("warning");
|
|
1789
|
+
const remaining = stats.contextRemaining == null ? null : Number(stats.contextRemaining);
|
|
1790
|
+
context.title = used == null
|
|
1791
|
+
? `${limit.toLocaleString()} token context limit; usage is being recalculated`
|
|
1792
|
+
: `${used.toLocaleString()} used · ${(remaining ?? Math.max(0, limit - used)).toLocaleString()} remaining · ${limit.toLocaleString()} limit`;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function thinkingRowInTab(tabState, text = "", label = "complete", before = null) {
|
|
1796
|
+
const node = document.createElement("details");
|
|
1797
|
+
node.className = "disclose think";
|
|
1798
|
+
node.open = true;
|
|
1799
|
+
node.innerHTML = `<summary><span class="chev">▶</span><span class="tool-name">Reasoning</span><span class="thinking-state muted">${esc(label)}</span></summary>`;
|
|
1800
|
+
const body = document.createElement("div");
|
|
1801
|
+
body.className = "body";
|
|
1802
|
+
body.textContent = text;
|
|
1803
|
+
node.appendChild(body);
|
|
1804
|
+
if (before) tabState.chatEl.insertBefore(node, before);
|
|
1805
|
+
else tabState.chatEl.appendChild(node);
|
|
1806
|
+
return node;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function finalizeThinking(tabState, text) {
|
|
1810
|
+
if (!tabState.thinkingEl && text?.trim()) tabState.thinkingEl = thinkingRowInTab(tabState, "", "complete", tabState.streamingEl);
|
|
1811
|
+
if (!tabState.thinkingEl) return;
|
|
1812
|
+
const body = tabState.thinkingEl.querySelector(".body");
|
|
1813
|
+
if (text?.trim()) body.textContent = text;
|
|
1814
|
+
if (!body.textContent.trim()) {
|
|
1815
|
+
tabState.thinkingEl.remove();
|
|
1816
|
+
tabState.thinkingEl = null;
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const label = tabState.thinkingEl.querySelector(".thinking-state");
|
|
1820
|
+
if (label) label.textContent = "complete";
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function appendHistoryMessage(tabState, role, text) {
|
|
1824
|
+
if (role === "user") setActivityTask(tabState, text);
|
|
1825
|
+
return role === "user" ? elInTab(tabState, "row-user", text) : assistantRowInTab(tabState, text);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function renderHistory(tabState, role, text, thinking = "") {
|
|
1829
|
+
if (role === "assistant" && thinking.trim()) thinkingRowInTab(tabState, thinking);
|
|
1830
|
+
const parts = String(text).split(/\[tool:(.*?)\]\s*(\S+)/g);
|
|
1831
|
+
if (parts.length === 1) {
|
|
1832
|
+
if (text.trim()) appendHistoryMessage(tabState, role, text);
|
|
1833
|
+
if (role === "assistant" && text.trim()) processAssistantForTasks(tabState, text);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (parts[0].trim()) appendHistoryMessage(tabState, role, parts[0].trim());
|
|
1837
|
+
for (let i = 1; i < parts.length; i += 3) {
|
|
1838
|
+
toolRowInTab(tabState, parts[i], parts[i + 1], "");
|
|
1839
|
+
if (parts[i + 2]?.trim()) appendHistoryMessage(tabState, role, parts[i + 2].trim());
|
|
1840
|
+
}
|
|
1841
|
+
if (role === "assistant") processAssistantForTasks(tabState, text);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function renderSnapshotItem(tabState, item) {
|
|
1845
|
+
if (!item) return;
|
|
1846
|
+
if (!item.type && item.role) {
|
|
1847
|
+
renderHistory(tabState, item.role, item.text ?? "", item.thinking ?? "");
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (item.type === "message") {
|
|
1851
|
+
renderHistory(tabState, item.role, item.text ?? "");
|
|
1852
|
+
} else if (item.type === "thinking" && item.text?.trim()) {
|
|
1853
|
+
thinkingRowInTab(tabState, item.text, "complete");
|
|
1854
|
+
} else if (item.type === "tool") {
|
|
1855
|
+
const args = item.args ?? {};
|
|
1856
|
+
const argsJson = JSON.stringify(args, null, 2);
|
|
1857
|
+
if (item.toolCallId) tabState.toolInputs[item.toolCallId] = args;
|
|
1858
|
+
const row = toolRowInTab(tabState, item.toolCallId, item.toolName, argsJson);
|
|
1859
|
+
if (item.output != null) finishTool(row, String(item.output), !!item.isError);
|
|
1860
|
+
else {
|
|
1861
|
+
const state = row.querySelector(".spin");
|
|
1862
|
+
if (state) state.textContent = "NO RESULT";
|
|
1863
|
+
}
|
|
1864
|
+
} else if (item.type === "error" && item.text) {
|
|
1865
|
+
el("note-error", item.text);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function handleTabEvent(state, event) {
|
|
1870
|
+
const isActive = state.id === activeTabId;
|
|
1871
|
+
switch (event.type) {
|
|
1872
|
+
case "init": {
|
|
1873
|
+
state.currentSessionFile = event.sessionFile;
|
|
1874
|
+
state.chatEl.innerHTML = "";
|
|
1875
|
+
state.streamingEl = state.thinkingEl = null;
|
|
1876
|
+
state.streamingText = "";
|
|
1877
|
+
state.toolInputs = {};
|
|
1878
|
+
state.activity.taskText = null;
|
|
1879
|
+
state.activity.tools = [];
|
|
1880
|
+
state.activity.tasks = [];
|
|
1881
|
+
for (const item of event.messages ?? []) renderSnapshotItem(state, item);
|
|
1882
|
+
if (event.stats) state.lastStats = event.stats;
|
|
1883
|
+
if (isActive) {
|
|
1884
|
+
currentSessionFile = state.currentSessionFile;
|
|
1885
|
+
$("ws-chip").innerHTML = iconFolder + `<span>${esc(event.cwd ?? "Unknown workspace")}</span>`;
|
|
1886
|
+
$("conv-title").textContent = event.sessionName || state.name || "New session";
|
|
1887
|
+
if (event.stats) setStats(event.stats);
|
|
1888
|
+
renderActivity();
|
|
1889
|
+
loadSessionList();
|
|
1890
|
+
loadGitStatus();
|
|
1891
|
+
scrollBottom();
|
|
1892
|
+
}
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
case "delta": {
|
|
1896
|
+
if (typeof event.delta !== "string" || event.delta.length === 0) break;
|
|
1897
|
+
if (!state.streamingEl) state.streamingEl = assistantRowInTab(state);
|
|
1898
|
+
state.streamingText += event.delta;
|
|
1899
|
+
setAssistantMarkdown(state.streamingEl, state.streamingText);
|
|
1900
|
+
if (isActive) scrollBottom();
|
|
1901
|
+
break;
|
|
1902
|
+
}
|
|
1903
|
+
case "thinking_delta": {
|
|
1904
|
+
if (typeof event.delta !== "string" || event.delta.length === 0) break;
|
|
1905
|
+
if (!state.thinkingEl) state.thinkingEl = thinkingRowInTab(state, "", "streaming");
|
|
1906
|
+
state.thinkingEl.querySelector(".body").textContent += event.delta;
|
|
1907
|
+
if (isActive) scrollBottom();
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
case "message_end": {
|
|
1911
|
+
if (event.role === "assistant") {
|
|
1912
|
+
finalizeThinking(state, event.thinking);
|
|
1913
|
+
const finalText = typeof event.text === "string" ? event.text : state.streamingText;
|
|
1914
|
+
if (finalText.trim()) {
|
|
1915
|
+
if (!state.streamingEl) state.streamingEl = assistantRowInTab(state);
|
|
1916
|
+
setAssistantMarkdown(state.streamingEl, finalText);
|
|
1917
|
+
} else if (state.streamingEl) {
|
|
1918
|
+
state.streamingEl.remove();
|
|
1919
|
+
}
|
|
1920
|
+
state.streamingEl = null;
|
|
1921
|
+
state.streamingText = "";
|
|
1922
|
+
state.thinkingEl = null;
|
|
1923
|
+
if (event.usage && (finalText.trim() || event.thinking?.trim())) elInTab(state, "usage-note", `${event.usage.input} in / ${event.usage.output} out · $${(+event.usage.cost).toFixed(4)}`);
|
|
1924
|
+
processAssistantForTasks(state, finalText);
|
|
1925
|
+
} else if (event.text?.trim()) {
|
|
1926
|
+
elInTab(state, "row-user", event.text);
|
|
1927
|
+
setActivityTask(state, event.text);
|
|
1928
|
+
}
|
|
1929
|
+
if (isActive) scrollBottom();
|
|
1930
|
+
break;
|
|
1931
|
+
}
|
|
1932
|
+
case "turn_end": elInTab(state, "turn-tail", "Turn complete"); if (isActive) { scrollBottom(); loadGitStatus(); } break;
|
|
1933
|
+
case "tool_start": {
|
|
1934
|
+
const args = JSON.stringify(event.args ?? {}, null, 2);
|
|
1935
|
+
if (event.toolCallId) state.toolInputs[event.toolCallId] = event.args ?? {};
|
|
1936
|
+
toolRowInTab(state, event.toolCallId, event.toolName, args);
|
|
1937
|
+
state.activity.tools.push({ id: event.toolCallId ?? "", name: event.toolName ?? "tool", state: "running" });
|
|
1938
|
+
if (isActive) renderActivity();
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
case "tool_end": {
|
|
1942
|
+
const selector = `[data-call-id="${CSS.escape(event.toolCallId ?? "")}"]`;
|
|
1943
|
+
const row = [...state.chatEl.querySelectorAll(selector)].pop();
|
|
1944
|
+
if (row) finishTool(row, event.output, event.isError, state);
|
|
1945
|
+
const tracked = [...state.activity.tools].reverse().find((tool) => tool.id ? tool.id === event.toolCallId : false)
|
|
1946
|
+
?? [...state.activity.tools].reverse().find((tool) => tool.state === "running");
|
|
1947
|
+
if (tracked) tracked.state = event.isError ? "error" : "done";
|
|
1948
|
+
if (isActive) { renderActivity(); scrollBottom(); }
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
case "stats":
|
|
1952
|
+
state.lastStats = event.stats;
|
|
1953
|
+
if (isActive) setStats(event.stats);
|
|
1954
|
+
break;
|
|
1955
|
+
case "session_meta":
|
|
1956
|
+
state.name = event.name || "New session";
|
|
1957
|
+
if (isActive) $("conv-title").textContent = state.name;
|
|
1958
|
+
loadSessionList();
|
|
1959
|
+
break;
|
|
1960
|
+
case "sessions_changed": loadSessionList(); break;
|
|
1961
|
+
case "info": elInTab(state, "note-row", event.text); break;
|
|
1962
|
+
case "error": elInTab(state, "note-error", event.error); break;
|
|
1963
|
+
case "status":
|
|
1964
|
+
state.runningFlag = event.status === "running" || event.status === "retrying";
|
|
1965
|
+
updateTabBarStreaming(state.id, state.runningFlag);
|
|
1966
|
+
if (isActive) setStatus(event.status);
|
|
1967
|
+
break;
|
|
1968
|
+
case "tab_closed": {
|
|
1969
|
+
// Server closed this tab
|
|
1970
|
+
destroyTabState(state.id);
|
|
1971
|
+
if (state.id === activeTabId && tabStates.size > 0) {
|
|
1972
|
+
switchToTab(tabStates.keys().next().value);
|
|
1973
|
+
}
|
|
1974
|
+
break;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
async function refreshRuntimeChips() {
|
|
1980
|
+
const tab = getActiveTab();
|
|
1981
|
+
const query = tab ? `?tab=${encodeURIComponent(tab.id)}` : "";
|
|
1982
|
+
const info = await (await fetch(`/api/info${query}`)).json();
|
|
1983
|
+
$("ws-chip").innerHTML = iconFolder + `<span>${esc(info.cwd)}</span>`;
|
|
1984
|
+
$("model-chip").innerHTML = `<span>${esc(info.model ?? "No model")}</span>`;
|
|
1985
|
+
$("act-model").innerHTML = iconModel + `<span title="${esc(info.model ?? "No model")}">${esc(info.model ?? "No model")}</span>`;
|
|
1986
|
+
$("act-cwd").innerHTML = iconFolder + `<span title="${esc(info.cwd)}">${esc(info.cwd)}</span>`;
|
|
1987
|
+
}
|
|
1988
|
+
refreshRuntimeChips().catch(() => {});
|
|
1989
|
+
fetch("/api/commands").then((response) => response.json()).then((data) => { commandCatalog = data; }).catch(() => {});
|
|
1990
|
+
|
|
1991
|
+
function detectCompletion() {
|
|
1992
|
+
const input = $("input");
|
|
1993
|
+
const cursor = input.selectionStart;
|
|
1994
|
+
const before = input.value.slice(0, cursor);
|
|
1995
|
+
const slash = before.match(/^\/([^\s]*)$/);
|
|
1996
|
+
if (slash) return { type: "command", query: slash[1], start: 0, end: cursor };
|
|
1997
|
+
const skill = before.match(/(?:^|\s)#([\w.-]*)$/);
|
|
1998
|
+
if (skill) return { type: "skill", query: skill[1], start: before.lastIndexOf("#"), end: cursor };
|
|
1999
|
+
const path = before.match(/(?:^|\s)@([^\s]*)$/);
|
|
2000
|
+
if (path) return { type: "path", query: path[1].replace(/^"/, ""), start: before.lastIndexOf("@"), end: cursor };
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function hideCompletions() {
|
|
2005
|
+
completionItems = [];
|
|
2006
|
+
completionToken = null;
|
|
2007
|
+
$("autocomplete").classList.remove("open");
|
|
2008
|
+
$("autocomplete").innerHTML = "";
|
|
2009
|
+
$("input").removeAttribute("aria-activedescendant");
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function renderCompletions(items, token) {
|
|
2013
|
+
completionItems = items.slice(0, 10);
|
|
2014
|
+
completionIndex = 0;
|
|
2015
|
+
completionToken = token;
|
|
2016
|
+
const menu = $("autocomplete");
|
|
2017
|
+
if (!completionItems.length) { hideCompletions(); return; }
|
|
2018
|
+
menu.innerHTML = completionItems.map((item, index) => `<button type="button" class="completion-item ${index === 0 ? "selected" : ""}" id="completion-${index}" data-index="${index}" role="option" aria-selected="${index === 0}"><span class="completion-value">${esc(item.value)}</span><span class="completion-description">${esc(item.description ?? "")}</span><span class="completion-source">${esc(item.source ?? "")}</span></button>`).join("");
|
|
2019
|
+
menu.classList.add("open");
|
|
2020
|
+
$("input").setAttribute("aria-activedescendant", "completion-0");
|
|
2021
|
+
menu.querySelectorAll(".completion-item").forEach((button) => {
|
|
2022
|
+
button.onmousedown = (event) => { event.preventDefault(); applyCompletion(Number(button.dataset.index)); };
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function selectCompletion(index) {
|
|
2027
|
+
if (!completionItems.length) return;
|
|
2028
|
+
completionIndex = (index + completionItems.length) % completionItems.length;
|
|
2029
|
+
const menu = $("autocomplete");
|
|
2030
|
+
menu.querySelectorAll(".completion-item").forEach((item, itemIndex) => {
|
|
2031
|
+
const selected = itemIndex === completionIndex;
|
|
2032
|
+
item.classList.toggle("selected", selected);
|
|
2033
|
+
item.setAttribute("aria-selected", String(selected));
|
|
2034
|
+
if (selected) {
|
|
2035
|
+
// Manual scroll-into-view (avoids scrollIntoView side effects in embedded frames)
|
|
2036
|
+
const top = item.offsetTop, bottom = top + item.offsetHeight;
|
|
2037
|
+
if (top < menu.scrollTop) menu.scrollTop = top;
|
|
2038
|
+
else if (bottom > menu.scrollTop + menu.clientHeight) menu.scrollTop = bottom - menu.clientHeight;
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
$("input").setAttribute("aria-activedescendant", `completion-${completionIndex}`);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function applyCompletion(index = completionIndex) {
|
|
2045
|
+
const item = completionItems[index];
|
|
2046
|
+
if (!item || !completionToken) return;
|
|
2047
|
+
const input = $("input");
|
|
2048
|
+
input.value = input.value.slice(0, completionToken.start) + item.insert + input.value.slice(completionToken.end);
|
|
2049
|
+
const cursor = completionToken.start + item.insert.length;
|
|
2050
|
+
input.setSelectionRange(cursor, cursor);
|
|
2051
|
+
hideCompletions();
|
|
2052
|
+
autoGrow();
|
|
2053
|
+
input.focus();
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
async function updateCompletions() {
|
|
2057
|
+
const token = detectCompletion();
|
|
2058
|
+
if (!token) { hideCompletions(); return; }
|
|
2059
|
+
const request = ++completionRequest;
|
|
2060
|
+
const query = token.query.toLowerCase();
|
|
2061
|
+
if (token.type === "command") {
|
|
2062
|
+
const items = commandCatalog.commands
|
|
2063
|
+
.filter((command) => `${command.name} ${command.description ?? ""}`.toLowerCase().includes(query))
|
|
2064
|
+
.map((command) => ({
|
|
2065
|
+
value: `/${command.name}${command.argumentHint ? ` ${command.argumentHint}` : ""}`,
|
|
2066
|
+
insert: `/${command.name} `,
|
|
2067
|
+
description: command.description,
|
|
2068
|
+
source: command.source,
|
|
2069
|
+
}));
|
|
2070
|
+
renderCompletions(items, token);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (token.type === "skill") {
|
|
2074
|
+
const items = commandCatalog.skills
|
|
2075
|
+
.filter((skill) => `${skill.name} ${skill.description ?? ""}`.toLowerCase().includes(query))
|
|
2076
|
+
.map((skill) => ({ value: `#${skill.name}`, insert: `#${skill.name} `, description: skill.description, source: "skill" }));
|
|
2077
|
+
renderCompletions(items, token);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
try {
|
|
2081
|
+
const response = await fetch(`/api/context/search?q=${encodeURIComponent(token.query)}&limit=80`);
|
|
2082
|
+
const data = await response.json();
|
|
2083
|
+
if (request !== completionRequest) return;
|
|
2084
|
+
const items = data.items.map((item) => {
|
|
2085
|
+
const display = `@${item.path}${item.type === "directory" ? "/" : ""}`;
|
|
2086
|
+
const referencedPath = item.path.includes(" ") ? `@"${item.path}${item.type === "directory" ? "/" : ""}"` : display;
|
|
2087
|
+
return { value: display, insert: referencedPath + " ", description: item.type, source: "workspace" };
|
|
2088
|
+
});
|
|
2089
|
+
renderCompletions(items, token);
|
|
2090
|
+
} catch { hideCompletions(); }
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function normalizeSkillAlias(text) {
|
|
2094
|
+
const match = text.match(/(^|\s)#([\w.-]+)\b/);
|
|
2095
|
+
if (!match) return text;
|
|
2096
|
+
const skill = commandCatalog.skills.find((item) => item.name.toLowerCase() === match[2].toLowerCase());
|
|
2097
|
+
if (!skill) return text;
|
|
2098
|
+
const start = match.index + match[1].length;
|
|
2099
|
+
const remainder = (text.slice(0, start) + text.slice(start + match[2].length + 1)).trim();
|
|
2100
|
+
return `/skill:${skill.name}${remainder ? " " + remainder : ""}`;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
async function runBuiltinCommand(name, args) {
|
|
2104
|
+
const response = await fetch("/api/command", {
|
|
2105
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ command: name, args, tabId: getActiveTab()?.id }),
|
|
2106
|
+
});
|
|
2107
|
+
const result = await response.json().catch(() => ({}));
|
|
2108
|
+
if (!response.ok) { el("note-error", result.error ?? `/${name} failed`); return; }
|
|
2109
|
+
if (result.action === "newtab") {
|
|
2110
|
+
createTabState(result.tabId, "New session");
|
|
2111
|
+
renderTabBar(result.tabs);
|
|
2112
|
+
switchToTab(result.tabId);
|
|
2113
|
+
}
|
|
2114
|
+
else if (result.action === "settings") openSettings("general");
|
|
2115
|
+
else if (result.action === "model") openModelPicker(result.query ?? args);
|
|
2116
|
+
else if (result.action === "resume") {
|
|
2117
|
+
document.body.classList.remove("sidebar-collapsed");
|
|
2118
|
+
if (matchMedia("(max-width: 1023px)").matches) document.body.classList.add("mobile-sidebar-open");
|
|
2119
|
+
el("note-row", "Choose a saved session from the sidebar");
|
|
2120
|
+
} else if (result.action === "session-info") {
|
|
2121
|
+
const info = result.info;
|
|
2122
|
+
el("note-row", `Session ${info.sessionId} · ${info.userMessages} user / ${info.assistantMessages} assistant · ${info.tokens.total.toLocaleString()} tokens · $${(+info.cost).toFixed(4)}`);
|
|
2123
|
+
} else if (result.action === "model-set") refreshRuntimeChips();
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
async function submit(mode) {
|
|
2127
|
+
let text = $("input").value.trim();
|
|
2128
|
+
if (!text) return;
|
|
2129
|
+
hideCompletions();
|
|
2130
|
+
const slash = text.match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
|
|
2131
|
+
const builtin = slash && webBuiltinNames.has(slash[1]);
|
|
2132
|
+
$("input").value = "";
|
|
2133
|
+
autoGrow();
|
|
2134
|
+
if (builtin) {
|
|
2135
|
+
await runBuiltinCommand(slash[1], slash[2]?.trim() ?? "");
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
text = normalizeSkillAlias(text);
|
|
2139
|
+
const active = getActiveTab();
|
|
2140
|
+
const response = await fetch("/api/prompt", {
|
|
2141
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
2142
|
+
body: JSON.stringify({ text, mode, tabId: active?.id }),
|
|
2143
|
+
});
|
|
2144
|
+
if (!response.ok) el("note-error", (await response.json().catch(() => ({}))).error ?? "Failed to send");
|
|
2145
|
+
}
|
|
2146
|
+
$("send").onclick = () => submit();
|
|
2147
|
+
$("steer-btn").onclick = () => submit("steer");
|
|
2148
|
+
$("abort").onclick = () => fetch("/api/abort", {
|
|
2149
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
2150
|
+
body: JSON.stringify({ tabId: getActiveTab()?.id }),
|
|
2151
|
+
});
|
|
2152
|
+
$("input").addEventListener("keydown", (event) => {
|
|
2153
|
+
if ($("autocomplete").classList.contains("open")) {
|
|
2154
|
+
if (event.key === "ArrowDown") { event.preventDefault(); selectCompletion(completionIndex + 1); return; }
|
|
2155
|
+
if (event.key === "ArrowUp") { event.preventDefault(); selectCompletion(completionIndex - 1); return; }
|
|
2156
|
+
if (event.key === "Tab" || (event.key === "Enter" && !event.shiftKey)) { event.preventDefault(); applyCompletion(); return; }
|
|
2157
|
+
if (event.key === "Escape") { event.preventDefault(); hideCompletions(); return; }
|
|
2158
|
+
}
|
|
2159
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
2160
|
+
event.preventDefault();
|
|
2161
|
+
submit(running && (event.metaKey || event.ctrlKey) ? "steer" : undefined);
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
function autoGrow() {
|
|
2165
|
+
$("input").style.height = "auto";
|
|
2166
|
+
$("input").style.height = Math.min($("input").scrollHeight, 230) + "px";
|
|
2167
|
+
}
|
|
2168
|
+
$("input").addEventListener("input", () => { autoGrow(); updateCompletions(); });
|
|
2169
|
+
$("input").addEventListener("click", updateCompletions);
|
|
2170
|
+
|
|
2171
|
+
function closeMobileNav() { document.body.classList.remove("mobile-sidebar-open"); }
|
|
2172
|
+
$("mobile-open").onclick = () => document.body.classList.add("mobile-sidebar-open");
|
|
2173
|
+
$("mobile-close").onclick = closeMobileNav;
|
|
2174
|
+
$("mobile-mask").onclick = closeMobileNav;
|
|
2175
|
+
$("sidebar-toggle").onclick = () => {
|
|
2176
|
+
document.body.classList.toggle("sidebar-collapsed");
|
|
2177
|
+
localStorage.setItem("pi-sidebar-collapsed", String(document.body.classList.contains("sidebar-collapsed")));
|
|
2178
|
+
};
|
|
2179
|
+
$("brand-mark").onclick = () => {
|
|
2180
|
+
if (document.body.classList.contains("sidebar-collapsed")) {
|
|
2181
|
+
document.body.classList.remove("sidebar-collapsed");
|
|
2182
|
+
localStorage.setItem("pi-sidebar-collapsed", "false");
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
if (localStorage.getItem("pi-sidebar-collapsed") === "true") document.body.classList.add("sidebar-collapsed");
|
|
2186
|
+
|
|
2187
|
+
// ── Theme preference: system / light / dark ──────────────────────────
|
|
2188
|
+
const THEME_ICONS = {
|
|
2189
|
+
system: '<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>',
|
|
2190
|
+
light: '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>',
|
|
2191
|
+
dark: '<svg class="icon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>',
|
|
2192
|
+
};
|
|
2193
|
+
const THEME_LABELS = { system: "Theme: system", light: "Theme: light", dark: "Theme: dark" };
|
|
2194
|
+
const themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
|
|
2195
|
+
let themePref = localStorage.getItem("pi-theme");
|
|
2196
|
+
if (!THEME_LABELS[themePref]) themePref = "system";
|
|
2197
|
+
function resolvedTheme() {
|
|
2198
|
+
return themePref === "system" ? (themeMedia.matches ? "dark" : "light") : themePref;
|
|
2199
|
+
}
|
|
2200
|
+
function applyTheme() {
|
|
2201
|
+
const mode = resolvedTheme();
|
|
2202
|
+
document.documentElement.dataset.theme = mode;
|
|
2203
|
+
document.querySelector('meta[name="theme-color"]').content = mode === "dark" ? "#0d1424" : "#f5f8ff";
|
|
2204
|
+
const toggle = $("theme-toggle");
|
|
2205
|
+
if (toggle) {
|
|
2206
|
+
toggle.innerHTML = THEME_ICONS[themePref];
|
|
2207
|
+
toggle.title = THEME_LABELS[themePref];
|
|
2208
|
+
toggle.setAttribute("aria-label", THEME_LABELS[themePref]);
|
|
2209
|
+
}
|
|
2210
|
+
document.querySelectorAll("[data-theme-option]").forEach((el) => el.classList.toggle("active", el.dataset.themeOption === themePref));
|
|
2211
|
+
}
|
|
2212
|
+
function setTheme(pref) {
|
|
2213
|
+
themePref = pref;
|
|
2214
|
+
try { localStorage.setItem("pi-theme", pref); } catch (_) {}
|
|
2215
|
+
applyTheme();
|
|
2216
|
+
}
|
|
2217
|
+
$("theme-toggle").onclick = () => setTheme(themePref === "system" ? "light" : themePref === "light" ? "dark" : "system");
|
|
2218
|
+
themeMedia.addEventListener("change", () => { if (themePref === "system") applyTheme(); });
|
|
2219
|
+
applyTheme();
|
|
2220
|
+
|
|
2221
|
+
async function loadSessionList() {
|
|
2222
|
+
const tabParam = activeTabId ? `?tab=${encodeURIComponent(activeTabId)}` : "";
|
|
2223
|
+
const data = await (await fetch("/api/sessions" + tabParam)).json();
|
|
2224
|
+
const list = $("session-list");
|
|
2225
|
+
list.innerHTML = "";
|
|
2226
|
+
if (data.currentName && data.current === currentSessionFile) {
|
|
2227
|
+
$("conv-title").textContent = data.currentName;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const createSessionItem = (label, file) => {
|
|
2231
|
+
if (!file) return null;
|
|
2232
|
+
const item = document.createElement("div");
|
|
2233
|
+
item.className = "sess" + (file === currentSessionFile ? " current" : "");
|
|
2234
|
+
item.title = file;
|
|
2235
|
+
item.tabIndex = 0;
|
|
2236
|
+
const copy = document.createElement("span");
|
|
2237
|
+
copy.textContent = label;
|
|
2238
|
+
const remove = document.createElement("button");
|
|
2239
|
+
remove.type = "button";
|
|
2240
|
+
remove.className = "session-delete";
|
|
2241
|
+
remove.textContent = "\u00d7";
|
|
2242
|
+
remove.title = `Move "${label}" to Trash`;
|
|
2243
|
+
remove.setAttribute("aria-label", `Delete session ${label}`);
|
|
2244
|
+
remove.onclick = async (event) => {
|
|
2245
|
+
event.stopPropagation();
|
|
2246
|
+
const activeWarning = file === currentSessionFile
|
|
2247
|
+
? " This is the active session; a new blank session will be opened first."
|
|
2248
|
+
: "";
|
|
2249
|
+
if (!window.confirm(`Move "${label}" to Trash?${activeWarning}`)) return;
|
|
2250
|
+
remove.disabled = true;
|
|
2251
|
+
const response = await fetch("/api/session/delete", {
|
|
2252
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ file, tabId: getActiveTab()?.id }),
|
|
2253
|
+
});
|
|
2254
|
+
if (!response.ok) {
|
|
2255
|
+
remove.disabled = false;
|
|
2256
|
+
alert("Delete failed: " + (await response.json()).error);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
await loadSessionList();
|
|
2260
|
+
};
|
|
2261
|
+
item.append(copy, remove);
|
|
2262
|
+
item.onclick = async () => {
|
|
2263
|
+
closeMobileNav();
|
|
2264
|
+
if (file === currentSessionFile) return;
|
|
2265
|
+
const response = await fetch("/api/session/switch", {
|
|
2266
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ file, tabId: getActiveTab()?.id }),
|
|
2267
|
+
});
|
|
2268
|
+
if (!response.ok) alert("Switch failed: " + (await response.json()).error);
|
|
2269
|
+
};
|
|
2270
|
+
item.onkeydown = (event) => {
|
|
2271
|
+
if ((event.key === "Enter" || event.key === " ") && event.target === item) { event.preventDefault(); item.onclick(); }
|
|
2272
|
+
};
|
|
2273
|
+
return item;
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
// Group sessions by workspace
|
|
2277
|
+
const seen = new Set();
|
|
2278
|
+
const groups = new Map();
|
|
2279
|
+
for (const item of [...(data.project ?? []), ...(data.all ?? [])]) {
|
|
2280
|
+
if (!item.file || seen.has(item.file)) continue;
|
|
2281
|
+
seen.add(item.file);
|
|
2282
|
+
const ws = item.workspace || "Other";
|
|
2283
|
+
if (!groups.has(ws)) groups.set(ws, []);
|
|
2284
|
+
groups.get(ws).push({ label: item.subject || item.name || item.firstMessage || fileLabel(item.file), file: item.file });
|
|
2285
|
+
if (seen.size >= 75) break;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
if (groups.size === 0) {
|
|
2289
|
+
const empty = document.createElement("div");
|
|
2290
|
+
empty.className = "nav-section-label sidebar-copy";
|
|
2291
|
+
empty.textContent = "No saved sessions";
|
|
2292
|
+
list.appendChild(empty);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// Read collapsed state from localStorage
|
|
2297
|
+
let collapsedGroups = new Set();
|
|
2298
|
+
try { collapsedGroups = new Set(JSON.parse(localStorage.getItem("pi-collapsed-ws") || "[]")); } catch {}
|
|
2299
|
+
|
|
2300
|
+
for (const [ws, sessions] of groups) {
|
|
2301
|
+
const group = document.createElement("div");
|
|
2302
|
+
group.className = "ws-group";
|
|
2303
|
+
|
|
2304
|
+
const header = document.createElement("div");
|
|
2305
|
+
header.className = "ws-group-header" + (collapsedGroups.has(ws) ? " collapsed" : "");
|
|
2306
|
+
header.innerHTML = `<span class="ws-chev">\u25B6</span><span class="ws-name">${esc(ws)}</span><span class="ws-count">${sessions.length}</span>`;
|
|
2307
|
+
header.onclick = () => {
|
|
2308
|
+
header.classList.toggle("collapsed");
|
|
2309
|
+
const current = new Set();
|
|
2310
|
+
document.querySelectorAll(".ws-group-header.collapsed").forEach((h) => {
|
|
2311
|
+
const name = h.querySelector(".ws-name")?.textContent;
|
|
2312
|
+
if (name) current.add(name);
|
|
2313
|
+
});
|
|
2314
|
+
try { localStorage.setItem("pi-collapsed-ws", JSON.stringify([...current])); } catch {}
|
|
2315
|
+
};
|
|
2316
|
+
group.appendChild(header);
|
|
2317
|
+
|
|
2318
|
+
const sessionsDiv = document.createElement("div");
|
|
2319
|
+
sessionsDiv.className = "ws-group-sessions";
|
|
2320
|
+
for (const s of sessions) {
|
|
2321
|
+
const el = createSessionItem(s.label, s.file);
|
|
2322
|
+
if (el) sessionsDiv.appendChild(el);
|
|
2323
|
+
}
|
|
2324
|
+
group.appendChild(sessionsDiv);
|
|
2325
|
+
list.appendChild(group);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function fileLabel(file) {
|
|
2329
|
+
const match = file.match(/(\d{4}-\d{2}-\d{2})T(\d{2}-\d{2})/);
|
|
2330
|
+
return match ? `${match[1]} ${match[2].replace("-", ":")}` : file.split("/").pop().slice(0, 34);
|
|
2331
|
+
}
|
|
2332
|
+
$("new-session").onclick = async () => {
|
|
2333
|
+
closeMobileNav();
|
|
2334
|
+
const response = await fetch("/api/session/new", {
|
|
2335
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
2336
|
+
body: JSON.stringify({ tabId: getActiveTab()?.id }),
|
|
2337
|
+
});
|
|
2338
|
+
if (!response.ok) alert("Failed: " + (await response.json()).error);
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
for (const button of document.querySelectorAll(".view-tabs button")) {
|
|
2342
|
+
button.onclick = () => {
|
|
2343
|
+
document.querySelectorAll(".view-tabs button").forEach((item) => item.classList.toggle("active", item === button));
|
|
2344
|
+
document.querySelectorAll(".view").forEach((view) => view.classList.toggle("active", view.id === "view-" + button.dataset.view));
|
|
2345
|
+
if (button.dataset.view === "traj") loadTraj();
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
async function loadTraj() {
|
|
2349
|
+
const data = await (await fetch("/api/logs?lines=300")).json();
|
|
2350
|
+
$("traj-body").innerHTML = "";
|
|
2351
|
+
for (const entry of [...(data.entries ?? [])].reverse()) {
|
|
2352
|
+
const node = document.createElement("div");
|
|
2353
|
+
node.className = "logline";
|
|
2354
|
+
node.innerHTML = `<div class="logmeta">${esc(entry.ts ?? "")} ${esc(entry.role ?? entry.type)}${entry.id ? " · " + esc(entry.id) : ""}${entry.usage ? " · tokens " + esc(entry.usage) : ""}</div><div style="white-space:pre-wrap">${esc(entry.preview)}</div>`;
|
|
2355
|
+
$("traj-body").appendChild(node);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
let wsBrowsePath = null;
|
|
2360
|
+
$("ws-chip").onclick = () => openWsBrowser();
|
|
2361
|
+
async function openWsBrowser(path) {
|
|
2362
|
+
const query = path ? "?path=" + encodeURIComponent(path) : "";
|
|
2363
|
+
const response = await fetch("/api/workspace/list" + query);
|
|
2364
|
+
if (!response.ok) { alert((await response.json()).error); return; }
|
|
2365
|
+
const data = await response.json();
|
|
2366
|
+
wsBrowsePath = data.current;
|
|
2367
|
+
const crumbs = data.ancestry.map((item) => `<span class="crumb" data-path="${esc(item.path)}">${esc(item.name)}</span>`).join('<span class="muted"> / </span>');
|
|
2368
|
+
const rows = data.entries.map((entry) => `<tr class="clickable ${entry.hidden ? "muted" : ""}" data-path="${esc(entry.path)}"><td>› ${esc(entry.name)}</td><td class="muted">${esc(entry.path)}</td></tr>`).join("");
|
|
2369
|
+
modalOpen(`<h2 class="sect">Choose workspace</h2><p id="workspace-crumbs">${crumbs}</p><p><button class="btn" id="use-workspace">Use this directory</button></p><table id="workspace-table"><tr><th>Directory</th><th>Path</th></tr>${rows || '<tr><td colspan="2" class="muted">No subdirectories</td></tr>'}</table>`, "workspace");
|
|
2370
|
+
$("workspace-crumbs").onclick = (event) => {
|
|
2371
|
+
const target = event.target.closest("[data-path]");
|
|
2372
|
+
if (target) openWsBrowser(target.dataset.path);
|
|
2373
|
+
};
|
|
2374
|
+
$("workspace-table").onclick = (event) => {
|
|
2375
|
+
const target = event.target.closest("tr[data-path]");
|
|
2376
|
+
if (target) openWsBrowser(target.dataset.path);
|
|
2377
|
+
};
|
|
2378
|
+
$("use-workspace").onclick = () => selectWorkspace(data.current);
|
|
2379
|
+
}
|
|
2380
|
+
async function selectWorkspace(path) {
|
|
2381
|
+
const response = await fetch("/api/workspace/set", {
|
|
2382
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path }),
|
|
2383
|
+
});
|
|
2384
|
+
if (!response.ok) { alert("Switch failed: " + (await response.json()).error); return; }
|
|
2385
|
+
closeModal();
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
let modalLock = false;
|
|
2389
|
+
function openSettings(section = "general") {
|
|
2390
|
+
closeMobileNav();
|
|
2391
|
+
$("mask").classList.add("open");
|
|
2392
|
+
$("modal").classList.remove("no-nav");
|
|
2393
|
+
$("modal").classList.add("open");
|
|
2394
|
+
modalLock = false;
|
|
2395
|
+
document.querySelectorAll("#modal nav button").forEach((button) => button.classList.toggle("active", button.dataset.sect === section));
|
|
2396
|
+
loadSection(section);
|
|
2397
|
+
}
|
|
2398
|
+
$("settings-btn").onclick = () => openSettings("general");
|
|
2399
|
+
$("model-chip").onclick = () => openModelPicker();
|
|
2400
|
+
$("mask").onclick = closeModal;
|
|
2401
|
+
document.addEventListener("keydown", (event) => {
|
|
2402
|
+
if (event.key === "Escape") {
|
|
2403
|
+
closeMobileNav();
|
|
2404
|
+
$("details").classList.remove("open");
|
|
2405
|
+
if (!modalLock) closeModal();
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
function closeModal() {
|
|
2409
|
+
modalLock = false;
|
|
2410
|
+
$("mask").classList.remove("open");
|
|
2411
|
+
$("modal").classList.remove("open");
|
|
2412
|
+
$("modal").classList.remove("no-nav");
|
|
2413
|
+
}
|
|
2414
|
+
function modalOpen(html, lock) {
|
|
2415
|
+
modalLock = lock === "workspace";
|
|
2416
|
+
$("mask").classList.add("open");
|
|
2417
|
+
$("modal").classList.add("no-nav");
|
|
2418
|
+
$("modal").classList.add("open");
|
|
2419
|
+
$("modal-body").innerHTML = html;
|
|
2420
|
+
document.querySelectorAll("#modal nav button").forEach((button) => button.classList.remove("active"));
|
|
2421
|
+
}
|
|
2422
|
+
for (const button of document.querySelectorAll("#modal nav button")) {
|
|
2423
|
+
button.onclick = () => {
|
|
2424
|
+
modalLock = false;
|
|
2425
|
+
document.querySelectorAll("#modal nav button").forEach((item) => item.classList.toggle("active", item === button));
|
|
2426
|
+
loadSection(button.dataset.sect);
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function filterModels(models, query) {
|
|
2431
|
+
const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
2432
|
+
return models.filter((model) => {
|
|
2433
|
+
const haystack = `${model.provider} ${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
2434
|
+
return terms.every((term) => haystack.includes(term));
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function modelRowsHtml(data, query) {
|
|
2439
|
+
return filterModels(data.available, query).map((model) => {
|
|
2440
|
+
const current = data.current && data.current.provider === model.provider && data.current.id === model.id;
|
|
2441
|
+
return `<tr class="${current ? "current " : ""}clickable" data-provider="${esc(model.provider)}" data-model="${esc(model.id)}"><td>${esc(model.provider + "/" + model.id)}${model.name && model.name !== model.id ? `<br><span class="muted">${esc(model.name)}</span>` : ""}</td><td>${model.reasoning ? "Yes" : "–"}</td><td>${model.contextWindow ? model.contextWindow.toLocaleString() : "–"}</td></tr>`;
|
|
2442
|
+
}).join("");
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
function bindModelChooser(data, closeOnPick) {
|
|
2446
|
+
const search = $("model-search");
|
|
2447
|
+
const body = $("model-table-body");
|
|
2448
|
+
const count = $("model-count");
|
|
2449
|
+
const render = () => {
|
|
2450
|
+
const filtered = filterModels(data.available, search.value);
|
|
2451
|
+
body.innerHTML = modelRowsHtml(data, search.value) || '<tr><td colspan="3" class="muted">No matching models</td></tr>';
|
|
2452
|
+
count.textContent = `${filtered.length} of ${data.available.length} models`;
|
|
2453
|
+
};
|
|
2454
|
+
search.oninput = render;
|
|
2455
|
+
body.onclick = async (event) => {
|
|
2456
|
+
const row = event.target.closest("tr[data-model]");
|
|
2457
|
+
if (row) await setModel(row.dataset.provider, row.dataset.model, closeOnPick);
|
|
2458
|
+
};
|
|
2459
|
+
render();
|
|
2460
|
+
search.focus();
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
async function openModelPicker(query = "") {
|
|
2464
|
+
const data = await (await fetch("/api/models")).json();
|
|
2465
|
+
modalOpen(`<h2 class="sect">Switch model</h2><p class="muted mono">Current: ${esc(data.current ? data.current.provider + "/" + data.current.id : "None")}</p><input id="model-search" class="search-input" type="search" value="${esc(query)}" placeholder="Search provider, model ID, or name…" autocomplete="off"><div id="model-count" class="model-count"></div><table><thead><tr><th>Model</th><th>Reasoning</th><th>Context</th></tr></thead><tbody id="model-table-body"></tbody></table>`, "model");
|
|
2466
|
+
bindModelChooser(data, true);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
const sections = {
|
|
2470
|
+
async general() {
|
|
2471
|
+
const info = await (await fetch("/api/info")).json();
|
|
2472
|
+
const themeButtons = ["system", "light", "dark"].map((mode) => `<button type="button" class="seg-btn${mode === themePref ? " active" : ""}" data-theme-option="${mode}">${mode[0].toUpperCase() + mode.slice(1)}</button>`).join("");
|
|
2473
|
+
$("modal-body").innerHTML = `<h2 class="sect">Appearance</h2><p class="muted" style="margin-top:-4px">Follows your system setting automatically in System mode.</p><div class="seg" role="radiogroup" aria-label="Theme">${themeButtons}</div><h2 class="sect">Workspace</h2><p>${esc(info.cwd)}</p><h2 class="sect">Session</h2><p class="mono muted" style="word-break:break-all">${esc(info.sessionFile ?? "No persisted session")}</p><h2 class="sect">Runtime</h2><table><tr><th>Model</th><th>Status</th></tr><tr><td>${esc(info.model ?? "None")}</td><td>${info.streaming ? "Running" : "Ready"}</td></tr></table>`;
|
|
2474
|
+
document.querySelectorAll("#modal-body [data-theme-option]").forEach((btn) => {
|
|
2475
|
+
btn.onclick = () => setTheme(btn.dataset.themeOption);
|
|
2476
|
+
});
|
|
2477
|
+
},
|
|
2478
|
+
async models() {
|
|
2479
|
+
const data = await (await fetch("/api/models")).json();
|
|
2480
|
+
const levels = ["off","minimal","low","medium","high","xhigh","max"];
|
|
2481
|
+
$("modal-body").innerHTML = `<h2 class="sect">Current model</h2><div>${esc(data.current ? data.current.provider + "/" + data.current.id : "None")} <span class="muted">${data.current?.contextWindow ? "· " + data.current.contextWindow.toLocaleString() + " context" : ""} · reasoning</span> <select id="thinking-level">${levels.map((level) => `<option ${level === data.thinkingLevel ? "selected" : ""}>${level}</option>`).join("")}</select></div><h2 class="sect">Available models</h2><input id="model-search" class="search-input" type="search" placeholder="Search provider, model ID, or name…" autocomplete="off"><div id="model-count" class="model-count"></div><table><thead><tr><th>Model</th><th>Reasoning</th><th>Context</th></tr></thead><tbody id="model-table-body"></tbody></table>`;
|
|
2482
|
+
$("thinking-level").onchange = (event) => setThinking(event.target.value);
|
|
2483
|
+
bindModelChooser(data, false);
|
|
2484
|
+
},
|
|
2485
|
+
async skills() {
|
|
2486
|
+
const data = await (await fetch("/api/skills")).json();
|
|
2487
|
+
const skills = data.skills ?? [], prompts = data.prompts ?? [], extensions = data.extensions ?? [];
|
|
2488
|
+
$("modal-body").innerHTML = `<h2 class="sect">Skills (${skills.length})</h2>${skills.length ? `<table><tr><th>Name</th><th>Description</th></tr>${skills.map((skill) => `<tr><td>${esc(skill.name)}</td><td>${esc(skill.description ?? "")}</td></tr>`).join("")}</table>` : '<p class="muted">No skills found.</p>'}<h2 class="sect">Prompt templates (${prompts.length})</h2>${prompts.length ? prompts.map((prompt) => `<div><span class="mono">/${esc(prompt.name)}</span> <span class="muted">${esc(prompt.description ?? "")}</span></div>`).join("") : '<p class="muted">None.</p>'}<h2 class="sect">Extensions (${extensions.length})</h2>${extensions.map((extension) => `<div class="mono muted">${esc(extension.path ?? "")}</div>`).join("") || '<p class="muted">None.</p>'}`;
|
|
2489
|
+
},
|
|
2490
|
+
async config() {
|
|
2491
|
+
const data = await (await fetch("/api/config")).json();
|
|
2492
|
+
$("modal-body").innerHTML = `<p class="muted mono">Agent directory: ${esc(data.agentDir)}</p>` + data.files.map((file) => `<h2 class="sect">${esc(file.label)}</h2>${file.data ? `<pre class="json">${esc(JSON.stringify(file.data, null, 2))}</pre>` : '<p class="muted">Not present</p>'}`).join("");
|
|
2493
|
+
},
|
|
2494
|
+
};
|
|
2495
|
+
async function loadSection(name) {
|
|
2496
|
+
try { await sections[name](); }
|
|
2497
|
+
catch (error) { $("modal-body").innerHTML = `<p style="color:var(--danger)">${esc(error.message)}</p>`; }
|
|
2498
|
+
}
|
|
2499
|
+
async function setModel(provider, id, closeOnPick = false) {
|
|
2500
|
+
const response = await fetch("/api/model/set", {
|
|
2501
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ provider, id, tabId: getActiveTab()?.id }),
|
|
2502
|
+
});
|
|
2503
|
+
if (!response.ok) {
|
|
2504
|
+
alert("Failed: " + (await response.json()).error);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
await refreshRuntimeChips();
|
|
2508
|
+
if (closeOnPick) closeModal();
|
|
2509
|
+
else loadSection("models");
|
|
2510
|
+
}
|
|
2511
|
+
async function setThinking(level) {
|
|
2512
|
+
await fetch("/api/thinking/set", {
|
|
2513
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ level, tabId: getActiveTab()?.id }),
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// ─── Parallel session tab bar ────────────────────────────────────────────
|
|
2518
|
+
function renderTabBar(tabsList) {
|
|
2519
|
+
const container = $("sidebar-tabs");
|
|
2520
|
+
if (!container) return;
|
|
2521
|
+
const label = $("tabs-label");
|
|
2522
|
+
// Only show tabs section when there are multiple parallel tabs
|
|
2523
|
+
if (tabsList.length <= 1) {
|
|
2524
|
+
container.innerHTML = "";
|
|
2525
|
+
if (label) label.hidden = true;
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (label) label.hidden = false;
|
|
2529
|
+
container.innerHTML = "";
|
|
2530
|
+
for (const tab of tabsList) {
|
|
2531
|
+
const item = document.createElement("div");
|
|
2532
|
+
item.className = "sidebar-tab" + (tab.id === activeTabId ? " active" : "") + (tab.streaming ? " streaming" : "");
|
|
2533
|
+
item.dataset.tabId = tab.id;
|
|
2534
|
+
const span = document.createElement("span");
|
|
2535
|
+
span.textContent = tab.name || "New session";
|
|
2536
|
+
item.appendChild(span);
|
|
2537
|
+
const closeBtn = document.createElement("button");
|
|
2538
|
+
closeBtn.type = "button";
|
|
2539
|
+
closeBtn.className = "tab-close-btn";
|
|
2540
|
+
closeBtn.textContent = "×";
|
|
2541
|
+
closeBtn.title = "Close tab";
|
|
2542
|
+
closeBtn.setAttribute("aria-label", "Close tab");
|
|
2543
|
+
closeBtn.onclick = (event) => { event.stopPropagation(); closeTab(tab.id); };
|
|
2544
|
+
item.appendChild(closeBtn);
|
|
2545
|
+
item.onclick = () => switchToTab(tab.id);
|
|
2546
|
+
container.appendChild(item);
|
|
2547
|
+
}
|
|
2548
|
+
// Add "New tab" button at the bottom
|
|
2549
|
+
const addBtn = document.createElement("button");
|
|
2550
|
+
addBtn.type = "button";
|
|
2551
|
+
addBtn.id = "sidebar-tab-add";
|
|
2552
|
+
addBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24" style="width:12px;height:12px"><path d="M12 5v14M5 12h14"/></svg> New tab';
|
|
2553
|
+
addBtn.onclick = openNewTab;
|
|
2554
|
+
container.appendChild(addBtn);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function updateTabBarLabel(tabId, name) {
|
|
2558
|
+
const item = document.querySelector(`#sidebar-tabs .sidebar-tab[data-tab-id="${CSS.escape(tabId)}"] span`);
|
|
2559
|
+
if (item) item.textContent = name || "New session";
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
function updateTabBarStreaming(tabId, streaming) {
|
|
2563
|
+
const item = document.querySelector(`#sidebar-tabs .sidebar-tab[data-tab-id="${CSS.escape(tabId)}"]`);
|
|
2564
|
+
if (item) item.classList.toggle("streaming", streaming);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
async function openNewTab() {
|
|
2568
|
+
const res = await fetch("/api/tabs/create", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
|
|
2569
|
+
const data = await res.json();
|
|
2570
|
+
if (!data.ok) { alert("Failed to create tab: " + (data.error || "unknown")); return; }
|
|
2571
|
+
const newTab = data.tabs.find((t) => t.id === data.tabId);
|
|
2572
|
+
createTabState(data.tabId, newTab?.name || "New session");
|
|
2573
|
+
renderTabBar(data.tabs);
|
|
2574
|
+
switchToTab(data.tabId);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
async function closeTab(id) {
|
|
2578
|
+
if (tabStates.size <= 1) return;
|
|
2579
|
+
const res = await fetch("/api/tabs/close", {
|
|
2580
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ tabId: id }),
|
|
2581
|
+
});
|
|
2582
|
+
const data = await res.json();
|
|
2583
|
+
if (!data.ok) { alert("Cannot close tab: " + (data.error || "unknown")); return; }
|
|
2584
|
+
const wasActive = activeTabId === id;
|
|
2585
|
+
destroyTabState(id);
|
|
2586
|
+
renderTabBar(data.tabs);
|
|
2587
|
+
if (wasActive && data.tabs.length > 0) {
|
|
2588
|
+
switchToTab(data.tabs[0].id);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
$("tab-add").onclick = openNewTab;
|
|
2592
|
+
|
|
2593
|
+
// Global SSE — server-side tab list changes (tabs opened/closed elsewhere)
|
|
2594
|
+
const globalEvents = new EventSource("/api/events/global");
|
|
2595
|
+
globalEvents.onmessage = (message) => {
|
|
2596
|
+
try {
|
|
2597
|
+
const event = JSON.parse(message.data);
|
|
2598
|
+
if (event.type === "tabs_changed") {
|
|
2599
|
+
renderTabBar(event.tabs);
|
|
2600
|
+
// Don't overwrite state.name with tab name — state.name tracks the session subject for the header
|
|
2601
|
+
}
|
|
2602
|
+
} catch (error) { console.error("Global event error", error); }
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
// Initialization: restore tabs from the server and activate the default one
|
|
2606
|
+
async function initTabs() {
|
|
2607
|
+
const res = await fetch("/api/tabs");
|
|
2608
|
+
const data = await res.json();
|
|
2609
|
+
for (const tab of data.tabs) {
|
|
2610
|
+
if (!tabStates.has(tab.id)) {
|
|
2611
|
+
createTabState(tab.id, tab.name);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
renderTabBar(data.tabs);
|
|
2615
|
+
if (data.tabs.length > 0) {
|
|
2616
|
+
switchToTab(data.defaultTab || data.tabs[0].id);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
initTabs().catch((e) => console.error("Init tabs failed", e));
|
|
2620
|
+
</script>
|
|
2621
|
+
</body>
|
|
2622
|
+
</html>
|