@logicseek/hello-cc 1.0.1 → 1.0.2
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/CHANGELOG.md +105 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +9 -0
- package/docs/commands.md +53 -0
- package/docs/commands.zh-CN.md +48 -0
- package/docs/dsh.md +137 -0
- package/docs/dsh.zh-CN.md +139 -0
- package/docs/mcp.md +9 -0
- package/docs/mcp.zh-CN.md +23 -0
- package/docs/native.md +295 -0
- package/docs/native.zh-CN.md +253 -0
- package/docs/web-handoff.zh-CN.md +53 -0
- package/lib/cli/app.mjs +25 -6
- package/lib/cli/commands/doctor.mjs +17 -2
- package/lib/cli/commands/dsh.mjs +79 -0
- package/lib/cli/commands/hook.mjs +241 -90
- package/lib/cli/commands/lock.mjs +34 -19
- package/lib/cli/commands/mcp.mjs +28 -0
- package/lib/cli/commands/native.mjs +130 -0
- package/lib/cli/commands/run.mjs +66 -21
- package/lib/cli/commands/task.mjs +6 -3
- package/lib/cli-args.mjs +4 -0
- package/lib/coordination-state.mjs +15 -1
- package/lib/core/coordination/event-retention.mjs +14 -2
- package/lib/core/coordination/gc-plan.mjs +3 -13
- package/lib/core/coordination/locks.mjs +45 -0
- package/lib/core/coordination/results.mjs +79 -0
- package/lib/core/peers/bindings.mjs +9 -3
- package/lib/diagnostics/codex.mjs +121 -0
- package/lib/integrations/codex-interactions.mjs +114 -0
- package/lib/integrations/dsh-collaboration.mjs +170 -0
- package/lib/integrations/dsh-cordis.d.ts +19 -0
- package/lib/integrations/dsh-cordis.mjs +177 -0
- package/lib/integrations/dsh.bundle.yml +7 -0
- package/lib/integrations/dsh.mjs +287 -0
- package/lib/integrations/native/claude.mjs +451 -0
- package/lib/integrations/native/codex.mjs +242 -0
- package/lib/integrations/native/dsh-acp.mjs +343 -0
- package/lib/integrations/native/index.mjs +19 -0
- package/lib/integrations/native/interactions.mjs +53 -0
- package/lib/integrations/native/jsonrpc.mjs +185 -0
- package/lib/integrations/peers/identity.mjs +36 -10
- package/lib/integrations/peers/processes.mjs +22 -8
- package/lib/integrations/providers.mjs +6 -2
- package/lib/mcp/scope.mjs +104 -0
- package/lib/mcp/stdio.mjs +80 -0
- package/lib/mcp/tools.mjs +197 -0
- package/lib/process/identity.mjs +3 -1
- package/lib/release/release-notes.mjs +9 -0
- package/lib/runtime/native/client.mjs +38 -0
- package/lib/runtime/native/service.mjs +511 -0
- package/lib/runtime/native/store.mjs +121 -0
- package/lib/ui/help.mjs +5 -2
- package/lib/web/adoption-state.mjs +55 -0
- package/lib/web/auto-attach.mjs +18 -2
- package/lib/web/codex-app-server.mjs +557 -0
- package/lib/web/codex-sessions.mjs +287 -0
- package/lib/web/control-lease.mjs +204 -0
- package/lib/web/http-routes.mjs +197 -15
- package/lib/web/http.mjs +5 -4
- package/lib/web/native-sessions.mjs +286 -0
- package/lib/web/project-contexts.mjs +2 -1
- package/lib/web/pty-sessions.mjs +60 -33
- package/lib/web/runtime-main.mjs +98 -6
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/task-results.mjs +76 -0
- package/lib/web/tmux-sessions.mjs +92 -11
- package/lib/web/tmux-stream.mjs +2 -2
- package/lib/web/ui-codex.mjs +550 -0
- package/lib/web/ui-command-palette.mjs +119 -0
- package/lib/web/ui-handoff.mjs +102 -0
- package/lib/web/ui-history.mjs +118 -0
- package/lib/web/ui-i18n.mjs +841 -0
- package/lib/web/ui-interactions.mjs +78 -0
- package/lib/web/ui-login.mjs +101 -0
- package/lib/web/ui-native.mjs +165 -0
- package/lib/web/ui-preferences.mjs +278 -0
- package/lib/web/ui-review.mjs +132 -0
- package/lib/web/ui-template.mjs +1106 -576
- package/lib/web/ui-terminal-find.mjs +98 -0
- package/lib/web/ui-workbench.mjs +94 -0
- package/lib/web/ui-workspace.mjs +222 -0
- package/package.json +11 -3
- package/scripts/check-free-identifiers.mjs +67 -8
- package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
- package/scripts/dsh-cordis-acceptance.mjs +200 -0
- package/scripts/dsh-installed-acceptance.mjs +247 -0
- package/scripts/dsh-official-acceptance.mjs +180 -0
- package/scripts/dsh-web-startup-acceptance.mjs +87 -0
- package/scripts/github-release.mjs +3 -1
- package/scripts/native-live-acceptance.mjs +369 -0
- package/scripts/regression.mjs +243 -89
- package/scripts/web-handoff-installed-acceptance.mjs +98 -0
- package/scripts/web-native-interaction-acceptance.mjs +204 -0
- package/scripts/web-native-model-acceptance.mjs +138 -0
package/lib/web/ui-template.mjs
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { API_VERSION } from './api-version.mjs';
|
|
2
|
+
import { uiPreferencesCss, uiPreferencesScript, preferenceControlsHtml, TERMINAL_THEMES } from './ui-preferences.mjs';
|
|
3
|
+
import { renderWebLoginPage } from './ui-login.mjs';
|
|
4
|
+
import { handoffCss, handoffBarHtml, terminalComposerHtml, uiHandoffScript } from './ui-handoff.mjs';
|
|
5
|
+
import { codexPanelHtml, codexPanelScript } from './ui-codex.mjs';
|
|
6
|
+
import { workbenchCss, workbenchControlsHtml, workbenchScript } from './ui-workbench.mjs';
|
|
7
|
+
import { codexHistoryHtml, codexHistoryScript } from './ui-history.mjs';
|
|
8
|
+
import { auxiliaryPanelsCss, reviewPanelHtml, reviewPanelScript } from './ui-review.mjs';
|
|
9
|
+
import { nativePanelHtml, nativePanelScript } from './ui-native.mjs';
|
|
10
|
+
import { commandPaletteCss, commandPaletteHtml, commandPaletteScript } from './ui-command-palette.mjs';
|
|
11
|
+
import { terminalFindCss, terminalFindHtml, terminalFindScript } from './ui-terminal-find.mjs';
|
|
12
|
+
import { workspaceCss, workspaceBarHtml, workspaceSecondaryHtml, workspaceScript } from './ui-workspace.mjs';
|
|
2
13
|
|
|
3
14
|
function nonceAttribute(nonce) {
|
|
4
15
|
const value = String(nonce || '');
|
|
@@ -8,7 +19,7 @@ function nonceAttribute(nonce) {
|
|
|
8
19
|
return ` nonce="${value}"`;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
|
-
export function webIndexHtml({ nonce } = {}) {
|
|
22
|
+
export function webIndexHtml({ nonce, pane = false } = {}) {
|
|
12
23
|
const inlineScriptNonce = nonceAttribute(nonce);
|
|
13
24
|
return `<!doctype html>
|
|
14
25
|
<html lang="en">
|
|
@@ -16,23 +27,16 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
16
27
|
<meta charset="utf-8">
|
|
17
28
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
29
|
<title>hello-cc</title>
|
|
30
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23087b65'/%3E%3Cpath d='m14 20 14 12-14 12m20 0h16' fill='none' stroke='white' stroke-width='5'/%3E%3C/svg%3E">
|
|
31
|
+
<script${inlineScriptNonce}>window.hccDraftScope = ${pane ? '"auxiliary"' : '""'}; document.documentElement.classList.toggle('session-pane', Boolean(window.hccDraftScope)); ${uiPreferencesScript()}</script>
|
|
19
32
|
<link rel="stylesheet" href="/assets/xterm.css">
|
|
20
33
|
<style>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
--text: #eef2f6;
|
|
28
|
-
--muted: #a3adba;
|
|
29
|
-
--accent: #40c4aa;
|
|
30
|
-
--warn: #f2bb4f;
|
|
31
|
-
--danger: #ff6b6b;
|
|
32
|
-
--ok: #75d17c;
|
|
33
|
-
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
34
|
-
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
35
|
-
}
|
|
34
|
+
${uiPreferencesCss}
|
|
35
|
+
${handoffCss}
|
|
36
|
+
${workbenchCss}
|
|
37
|
+
${auxiliaryPanelsCss}
|
|
38
|
+
${commandPaletteCss}
|
|
39
|
+
${terminalFindCss}
|
|
36
40
|
* { box-sizing: border-box; }
|
|
37
41
|
body {
|
|
38
42
|
margin: 0;
|
|
@@ -49,19 +53,21 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
49
53
|
border: 1px solid var(--border);
|
|
50
54
|
background: var(--panel-2);
|
|
51
55
|
color: var(--text);
|
|
52
|
-
height:
|
|
56
|
+
height: var(--control-height);
|
|
53
57
|
border-radius: 6px;
|
|
54
58
|
padding: 0 10px;
|
|
55
59
|
cursor: pointer;
|
|
56
60
|
}
|
|
57
61
|
button:hover { border-color: var(--accent); }
|
|
58
|
-
button.primary { background:
|
|
59
|
-
button.danger { background:
|
|
62
|
+
button.primary { background: var(--primary-bg); border-color: var(--primary-border); }
|
|
63
|
+
button.danger { background: var(--danger-bg); border-color: var(--danger-border); }
|
|
64
|
+
button:disabled { opacity: .55; cursor: default; }
|
|
65
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
60
66
|
input, select {
|
|
61
67
|
width: 100%;
|
|
62
|
-
height:
|
|
68
|
+
height: var(--control-height);
|
|
63
69
|
border: 1px solid var(--border);
|
|
64
|
-
background:
|
|
70
|
+
background: var(--input-bg);
|
|
65
71
|
color: var(--text);
|
|
66
72
|
border-radius: 6px;
|
|
67
73
|
padding: 0 9px;
|
|
@@ -75,12 +81,12 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
75
81
|
}
|
|
76
82
|
.app {
|
|
77
83
|
position: relative;
|
|
78
|
-
height:
|
|
84
|
+
height: calc(100dvh - 58px);
|
|
79
85
|
display: grid;
|
|
80
86
|
--left-width: 320px;
|
|
81
87
|
--right-width: 360px;
|
|
82
88
|
grid-template-columns: var(--left-width) minmax(0, 1fr) var(--right-width);
|
|
83
|
-
min-width:
|
|
89
|
+
min-width: 0;
|
|
84
90
|
transition: grid-template-columns .18s ease;
|
|
85
91
|
}
|
|
86
92
|
.app.resizing { transition: none; }
|
|
@@ -135,7 +141,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
135
141
|
transition: left .18s ease, right .18s ease, color .12s, border-color .12s, background .12s;
|
|
136
142
|
}
|
|
137
143
|
.app.resizing .edge-toggle { transition: none; }
|
|
138
|
-
.edge-toggle:hover { color: var(--text); border-color: var(--accent); background:
|
|
144
|
+
.edge-toggle:hover { color: var(--text); border-color: var(--accent); background: var(--hover-bg); }
|
|
139
145
|
.edge-left { left: var(--left-width); transform: translate(-50%, -50%); }
|
|
140
146
|
.edge-right { right: var(--right-width); transform: translate(50%, -50%); }
|
|
141
147
|
.app.left-collapsed .edge-left { left: 0; transform: translate(0, -50%); }
|
|
@@ -148,12 +154,12 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
148
154
|
background: var(--panel);
|
|
149
155
|
border-right: 1px solid var(--border);
|
|
150
156
|
display: grid;
|
|
151
|
-
grid-template-rows: auto auto auto
|
|
157
|
+
grid-template-rows: auto auto auto minmax(0, 1fr);
|
|
152
158
|
}
|
|
153
159
|
.inspector {
|
|
154
160
|
border-right: 0;
|
|
155
161
|
border-left: 1px solid var(--border);
|
|
156
|
-
grid-template-rows: auto 1fr;
|
|
162
|
+
grid-template-rows: auto auto auto minmax(0, 1fr);
|
|
157
163
|
}
|
|
158
164
|
.sidebar > *, .inspector > * {
|
|
159
165
|
min-width: 0;
|
|
@@ -214,9 +220,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
214
220
|
.session-header {
|
|
215
221
|
border-bottom: 1px solid var(--border);
|
|
216
222
|
padding: 8px 10px;
|
|
217
|
-
display:
|
|
218
|
-
align-items: center;
|
|
219
|
-
justify-content: space-between;
|
|
223
|
+
display: grid;
|
|
220
224
|
gap: 8px;
|
|
221
225
|
min-height: 48px;
|
|
222
226
|
}
|
|
@@ -224,9 +228,32 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
224
228
|
font-size: 13px;
|
|
225
229
|
font-weight: 600;
|
|
226
230
|
}
|
|
227
|
-
.
|
|
228
|
-
|
|
229
|
-
}
|
|
231
|
+
.project-picker { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 8px; }
|
|
232
|
+
.project-picker button { width: var(--control-height); padding: 0; font-size: 20px; }
|
|
233
|
+
.session-filters { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
234
|
+
.session-list-count { color: var(--muted); font-size: 11px; min-height: 15px; }
|
|
235
|
+
.session-title { flex: 1; min-width: 0; }
|
|
236
|
+
.session-subtitle { display: flex; align-items: center; gap: 7px; min-width: 0; color: var(--muted); font-size: 11px; }
|
|
237
|
+
.session-subtitle .path { flex: 1; min-width: 0; }
|
|
238
|
+
.session-provider { flex: 0 0 auto; font-family: var(--mono); }
|
|
239
|
+
.session-details { min-width: 0; color: var(--muted); font-size: 11px; cursor: auto; }
|
|
240
|
+
.session-details summary { width: fit-content; max-width: 100%; cursor: pointer; padding: 3px 0; }
|
|
241
|
+
.session-details[open] summary { margin-bottom: 6px; }
|
|
242
|
+
.session-details dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 5px 8px; margin: 0; }
|
|
243
|
+
.session-details dt { color: var(--muted); }
|
|
244
|
+
.session-details dd { margin: 0; color: var(--text); font-family: var(--mono); overflow-wrap: anywhere; }
|
|
245
|
+
.session-action { flex: 0 0 auto; width: 36px; height: 36px; padding: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--muted); background: transparent; }
|
|
246
|
+
.session-action[data-action="restart-detected"] { color: var(--ok); }
|
|
247
|
+
.dialog-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
248
|
+
.dialog-heading button { width: 36px; height: 36px; padding: 0; font-size: 20px; }
|
|
249
|
+
.dialog-help { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
|
|
250
|
+
.dialog .form { padding: 0; border: 0; gap: 14px; }
|
|
251
|
+
#startForm [hidden], .dialog-help[hidden] { display: none; }
|
|
252
|
+
.dialog-help[role="alert"] { color: var(--danger); }
|
|
253
|
+
.dialog-target { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
|
|
254
|
+
.session-empty { display: grid; gap: 12px; text-align: center; }
|
|
255
|
+
.session-empty p { margin: 0; }
|
|
256
|
+
@media (pointer: coarse) { .session-action { width: 44px; height: 44px; } .session-details summary { padding: 10px 0; } }
|
|
230
257
|
.sessions, .state {
|
|
231
258
|
min-height: 0;
|
|
232
259
|
overflow-y: auto;
|
|
@@ -244,7 +271,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
244
271
|
.sessions::-webkit-scrollbar-thumb, .state::-webkit-scrollbar-thumb { background: #3a3f4a; border-radius: 4px; }
|
|
245
272
|
.session {
|
|
246
273
|
border: 1px solid var(--border);
|
|
247
|
-
background:
|
|
274
|
+
background: var(--card-bg);
|
|
248
275
|
border-radius: 8px;
|
|
249
276
|
padding: 9px;
|
|
250
277
|
display: grid;
|
|
@@ -253,6 +280,8 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
253
280
|
cursor: pointer;
|
|
254
281
|
}
|
|
255
282
|
.session.active { border-color: var(--accent); }
|
|
283
|
+
.session-select { min-width: 0; height: auto; padding: 0; border: 0; background: transparent; color: var(--text); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
284
|
+
.session-select strong { font-size: var(--body-font); }
|
|
256
285
|
.row {
|
|
257
286
|
display: flex;
|
|
258
287
|
align-items: center;
|
|
@@ -282,9 +311,9 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
282
311
|
min-height: 0;
|
|
283
312
|
min-width: 0;
|
|
284
313
|
overflow: hidden;
|
|
285
|
-
display:
|
|
286
|
-
|
|
287
|
-
background:
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-direction: column;
|
|
316
|
+
background: var(--terminal-bg);
|
|
288
317
|
}
|
|
289
318
|
.toolbar {
|
|
290
319
|
min-height: 48px;
|
|
@@ -356,7 +385,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
356
385
|
border-radius: 6px;
|
|
357
386
|
color: var(--text);
|
|
358
387
|
}
|
|
359
|
-
.menu button:hover { background:
|
|
388
|
+
.menu button:hover { background: var(--hover-bg); }
|
|
360
389
|
.menu .divider {
|
|
361
390
|
height: 1px;
|
|
362
391
|
margin: 4px 2px;
|
|
@@ -396,19 +425,19 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
396
425
|
font-family: var(--mono);
|
|
397
426
|
font-size: 11px;
|
|
398
427
|
color: var(--text);
|
|
399
|
-
background:
|
|
428
|
+
background: var(--input-bg);
|
|
400
429
|
}
|
|
401
430
|
/* Confirm dialog overlay */
|
|
402
431
|
.dialog-overlay {
|
|
403
432
|
position: fixed; inset: 0; z-index: 2000;
|
|
404
|
-
background:
|
|
433
|
+
background: var(--overlay);
|
|
405
434
|
display: flex; align-items: center; justify-content: center;
|
|
406
435
|
}
|
|
407
436
|
.dialog-overlay[hidden] { display: none; }
|
|
408
437
|
.dialog {
|
|
409
438
|
background: var(--panel); border: 1px solid var(--border);
|
|
410
439
|
border-radius: 10px; padding: 20px 22px;
|
|
411
|
-
|
|
440
|
+
width: min(440px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto;
|
|
412
441
|
display: grid; gap: 14px;
|
|
413
442
|
box-shadow: 0 12px 40px rgba(0,0,0,.5);
|
|
414
443
|
}
|
|
@@ -419,11 +448,14 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
419
448
|
#terminal {
|
|
420
449
|
min-height: 0;
|
|
421
450
|
overflow: hidden;
|
|
422
|
-
padding:
|
|
451
|
+
padding: 0;
|
|
423
452
|
}
|
|
424
453
|
#terminal .xterm {
|
|
425
454
|
cursor: default;
|
|
455
|
+
padding: 8px;
|
|
456
|
+
background: inherit;
|
|
426
457
|
}
|
|
458
|
+
#terminal .xterm-viewport { background: inherit; }
|
|
427
459
|
/* The terminal mirrors a tmux pane; keep xterm's own hidden helper textarea
|
|
428
460
|
off-screen, but DO show the rendered block cursor (positioned from tmux). */
|
|
429
461
|
#terminal .xterm-helper-textarea {
|
|
@@ -439,7 +471,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
439
471
|
.card {
|
|
440
472
|
border: 1px solid var(--border);
|
|
441
473
|
border-radius: 8px;
|
|
442
|
-
background:
|
|
474
|
+
background: var(--card-bg);
|
|
443
475
|
overflow: hidden;
|
|
444
476
|
}
|
|
445
477
|
.card.state-card {
|
|
@@ -478,7 +510,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
478
510
|
cursor: pointer;
|
|
479
511
|
text-align: left;
|
|
480
512
|
}
|
|
481
|
-
.state-card-toggle:hover { background:
|
|
513
|
+
.state-card-toggle:hover { background: var(--hover-bg); }
|
|
482
514
|
.state-card-toggle-title {
|
|
483
515
|
min-width: 0;
|
|
484
516
|
display: flex;
|
|
@@ -525,7 +557,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
525
557
|
gap: 2px;
|
|
526
558
|
font-size: 12px;
|
|
527
559
|
color: var(--muted);
|
|
528
|
-
border-bottom: 1px solid
|
|
560
|
+
border-bottom: 1px solid var(--border);
|
|
529
561
|
padding-bottom: 6px;
|
|
530
562
|
}
|
|
531
563
|
.item:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
@@ -537,54 +569,110 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
537
569
|
.sec-spacer { flex: 1 1 auto; }
|
|
538
570
|
.sec-label button { height: 22px; padding: 0 7px; font-size: 10px; text-transform: none; letter-spacing: 0; }
|
|
539
571
|
#terminal { display: flex; flex-direction: column; }
|
|
572
|
+
body { font-size: var(--body-font); }
|
|
573
|
+
.path, .brand span, .badge, .action-result pre, .sec-label { font-size: var(--small-font); }
|
|
574
|
+
.form, .brand, .sessions, .state, .session { padding: var(--space); }
|
|
575
|
+
.form, .sessions, .state { gap: var(--space); }
|
|
576
|
+
.global-header { height: 58px; padding: 0 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
577
|
+
.global-header > strong { margin-right: auto; white-space: nowrap; font-size: 16px; letter-spacing: -.3px; }
|
|
578
|
+
.header-controls { display: flex; align-items: center; gap: 8px; }
|
|
579
|
+
.header-controls button { white-space: nowrap; }
|
|
580
|
+
.global-header #connState { max-width: none; font-size: var(--small-font); color: var(--muted); white-space: nowrap; }
|
|
581
|
+
.global-header #logoutBtn { width: 34px; padding: 0; }
|
|
582
|
+
.mobile-nav { display: none; }
|
|
583
|
+
.settings-fields { display: grid; gap: 16px; }
|
|
584
|
+
.settings-fields label { grid-template-columns: minmax(0, 1fr) minmax(150px, 1fr); align-items: center; gap: 12px; font-size: 13px; }
|
|
585
|
+
.settings-dialog header { display: flex; align-items: center; justify-content: space-between; }
|
|
586
|
+
.settings-dialog header button { width: 32px; padding: 0; }
|
|
587
|
+
.app.focus-mode { grid-template-columns: 0 minmax(0, 1fr) 0; }
|
|
588
|
+
.app.focus-mode .sidebar, .app.focus-mode .inspector { visibility: hidden; border: 0; }
|
|
589
|
+
.app.focus-mode .edge-toggle, .app.focus-mode .edge-resizer { display: none; }
|
|
590
|
+
.global-header button[aria-pressed="true"], .mobile-nav button[aria-current="page"] { background: var(--selection-bg); border-color: var(--accent); }
|
|
591
|
+
input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); }
|
|
592
|
+
@media (max-width: 1099px) {
|
|
593
|
+
.global-header { padding: 0 12px; gap: 8px; }
|
|
594
|
+
.mobile-nav { height: 46px; display: flex; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
595
|
+
.mobile-nav button { flex: 1; height: 32px; }
|
|
596
|
+
.app, .app.left-collapsed, .app.right-collapsed, .app.left-collapsed.right-collapsed, .app.focus-mode { height: calc(100dvh - 104px); grid-template-columns: minmax(0, 1fr); }
|
|
597
|
+
.app .sidebar, .app .main, .app .inspector { display: none; border: 0; visibility: visible; }
|
|
598
|
+
.app[data-view="sessions"] .sidebar, .app[data-view="state"] .inspector { display: grid; }
|
|
599
|
+
.app[data-view="terminal"] .main { display:flex; }
|
|
600
|
+
.app .edge-toggle, .app .edge-resizer { display: none; }
|
|
601
|
+
#focusBtn { display: none; }
|
|
602
|
+
.sidebar { grid-template-rows: auto auto auto minmax(0, 1fr); }
|
|
603
|
+
}
|
|
604
|
+
@media (max-width: 480px) {
|
|
605
|
+
.global-header { padding: 0 10px; }
|
|
606
|
+
.global-header > strong { font-size: 15px; }
|
|
607
|
+
.header-controls { gap: 6px; }
|
|
608
|
+
.global-header #connState { max-width: 88px; overflow: hidden; text-overflow: ellipsis; }
|
|
609
|
+
.toolbar { flex-wrap: wrap; padding: 8px 10px; }
|
|
610
|
+
.toolbar .title { flex-basis: 100%; }
|
|
611
|
+
.quick { margin-left: auto; }
|
|
612
|
+
.settings-fields label { grid-template-columns: 1fr; gap: 6px; }
|
|
613
|
+
}
|
|
614
|
+
@media (prefers-reduced-motion: reduce) { .app, .edge-toggle, .edge-resizer { transition: none; } }
|
|
615
|
+
${workspaceCss}
|
|
540
616
|
</style>
|
|
541
617
|
</head>
|
|
542
618
|
<body>
|
|
543
|
-
<
|
|
619
|
+
<header class="global-header">
|
|
620
|
+
<strong>hello-cc</strong>
|
|
621
|
+
<div class="header-controls">
|
|
622
|
+
<span id="connState" role="status">offline</span>
|
|
623
|
+
<button id="commandsBtn" type="button" data-i18n="commands.button" data-i18n-title="commands.shortcutHint" aria-haspopup="dialog" aria-controls="commandDialog" aria-keyshortcuts="Control+Shift+P Meta+Shift+P">Commands</button>
|
|
624
|
+
<button id="splitBtn" type="button" data-i18n="workspace.split" aria-pressed="false" disabled>Split</button>
|
|
625
|
+
<button id="focusBtn" type="button" data-i18n="focusMode" aria-pressed="false">Focus</button>
|
|
626
|
+
<button id="settingsBtn" type="button" data-i18n="settings" aria-haspopup="dialog" aria-controls="settingsDialog">Settings</button>
|
|
627
|
+
<button id="logoutBtn" type="button" data-i18n-title="logout" data-i18n-aria="logout" title="Log out" aria-label="Log out">↪</button>
|
|
628
|
+
</div>
|
|
629
|
+
</header>
|
|
630
|
+
<nav class="mobile-nav" data-i18n-aria="navigation" aria-label="Navigation">
|
|
631
|
+
<button type="button" data-view="sessions" data-i18n="sessions">Sessions</button>
|
|
632
|
+
<button type="button" data-view="terminal" data-i18n="terminal" aria-current="page">Terminal</button>
|
|
633
|
+
<button type="button" data-view="state" data-i18n="projectState">Project State</button>
|
|
634
|
+
</nav>
|
|
635
|
+
<div class="app" data-view="terminal">
|
|
544
636
|
<aside class="sidebar">
|
|
545
637
|
<div class="brand">
|
|
546
638
|
<div>
|
|
547
|
-
<strong>
|
|
639
|
+
<strong data-i18n="project">Project</strong>
|
|
548
640
|
<div class="path" id="rootPath"></div>
|
|
549
641
|
</div>
|
|
550
|
-
<div class="brand-actions">
|
|
551
|
-
<span id="connState" data-i18n="conn.offline">offline</span>
|
|
552
|
-
<button id="logoutBtn" type="button" data-i18n-title="logout" data-i18n-aria="logout" title="Log out" aria-label="Log out">↪</button>
|
|
553
|
-
</div>
|
|
554
642
|
</div>
|
|
555
643
|
<div class="form" style="padding-top:10px;padding-bottom:10px">
|
|
556
|
-
<div class="
|
|
644
|
+
<div class="project-picker">
|
|
557
645
|
<label><span data-i18n="project">Project</span><select id="projectSelect"></select></label>
|
|
558
|
-
<
|
|
646
|
+
<button id="openProjectDialog" type="button" aria-haspopup="dialog" aria-controls="projectDialog" data-i18n-title="addProject" data-i18n-aria="addProject" title="Add project" aria-label="Add project">+</button>
|
|
559
647
|
</div>
|
|
560
|
-
<
|
|
561
|
-
<button id="
|
|
648
|
+
<button id="openStartDialog" class="primary" type="button" data-i18n="newSession" aria-haspopup="dialog" aria-controls="startDialog">New session</button>
|
|
649
|
+
<button id="openHistoryDialog" type="button" data-i18n="aux.history" aria-haspopup="dialog" aria-controls="historyDialog">Codex history</button>
|
|
562
650
|
</div>
|
|
563
|
-
<form class="form" id="startForm">
|
|
564
|
-
<div class="start-row">
|
|
565
|
-
<label><span data-i18n="newSession">New session</span><select id="kind"><option value="codex">codex</option><option value="claude">claude</option><option value="shell" data-i18n="kind.shell">shell</option></select></label>
|
|
566
|
-
<button class="primary" type="submit" data-i18n="start">Start</button>
|
|
567
|
-
</div>
|
|
568
|
-
<div class="start-options">
|
|
569
|
-
<label><span data-i18n="mode">Mode</span><select id="startMode"><option value="new" data-i18n="mode.new">new</option><option value="resume" data-i18n="mode.resume">resume</option><option value="last" data-i18n="mode.last">last</option><option value="continue" data-i18n="mode.continue">continue</option></select></label>
|
|
570
|
-
<label data-resume-field><span data-i18n="session">Session</span><select id="resumeSelect"></select></label>
|
|
571
|
-
<label data-resume-field data-resume-custom style="display:none"><span data-i18n="sessionId">Session id</span><input id="resumeArg" data-i18n-placeholder="sessionIdPlaceholder" placeholder="session id or name"></label>
|
|
572
|
-
</div>
|
|
573
|
-
</form>
|
|
574
651
|
<div class="session-header">
|
|
575
652
|
<strong data-i18n="sessions">Sessions</strong>
|
|
576
|
-
<
|
|
653
|
+
<input id="sessionSearch" type="search" data-i18n-placeholder="sessionSearchPlaceholder" data-i18n-aria="sessionSearch" aria-label="Search sessions" placeholder="Name, ID, directory or command…" autocomplete="off">
|
|
654
|
+
<div class="session-filters">
|
|
655
|
+
<label><span data-i18n="view">View</span><select id="sessionKindFilter"><option value="all" data-i18n="kind.all">all</option><option value="claude">claude</option><option value="codex">codex</option><option value="dsh">dsh</option><option value="shell" data-i18n="kind.shell">shell</option><option value="other" data-i18n="kind.other">other</option></select></label>
|
|
656
|
+
<label><span data-i18n="sessionStatusFilter">Session availability</span><select id="sessionStatusFilter"><option value="all" data-i18n="sessionFilterAll">All sessions</option><option value="active" data-i18n="sessionFilterActive">Active sessions</option></select></label>
|
|
657
|
+
</div>
|
|
658
|
+
<span class="session-list-count" id="sessionListCount" role="status" aria-live="polite"></span>
|
|
577
659
|
</div>
|
|
578
660
|
<div class="sessions" id="sessions"></div>
|
|
579
661
|
</aside>
|
|
580
662
|
|
|
581
|
-
<
|
|
663
|
+
<section class="workspace" id="workspace" data-active-pane="primary">
|
|
664
|
+
${workspaceBarHtml()}
|
|
665
|
+
<div class="workspace-panes" id="workspacePanes">
|
|
666
|
+
<main class="main" id="workspacePrimaryPane">
|
|
582
667
|
<div class="toolbar">
|
|
583
668
|
<div class="title">
|
|
584
|
-
<strong id="activeTitle"
|
|
585
|
-
<span class="path" id="activeMeta"
|
|
669
|
+
<strong id="activeTitle">No session selected</strong>
|
|
670
|
+
<span class="path" id="activeMeta">Start or select a session from the left panel</span>
|
|
671
|
+
<span id="activeTask" hidden></span>
|
|
586
672
|
</div>
|
|
587
673
|
<div class="quick" id="quickBar">
|
|
674
|
+
<button id="openReviewDialog" type="button" data-i18n="aux.review" aria-haspopup="dialog" aria-controls="reviewDialog" disabled>Review results</button>
|
|
675
|
+
<button id="terminalFindBtn" type="button" data-i18n="find.button" data-i18n-title="find.shortcutHint" aria-controls="terminalFind" aria-expanded="false" aria-keyshortcuts="Control+Shift+F Meta+Shift+F" disabled>Find</button>
|
|
588
676
|
<div class="menu-wrap">
|
|
589
677
|
<button class="menu-btn" id="actionsBtn" type="button" aria-haspopup="true" aria-expanded="false"><span data-i18n="actions">Actions</span> ▾</button>
|
|
590
678
|
<div class="menu" id="actionsMenu" hidden>
|
|
@@ -598,17 +686,25 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
598
686
|
<button data-terminal-action="status" data-i18n="action.runStatusTerminal">run status in terminal</button>
|
|
599
687
|
</div>
|
|
600
688
|
</div>
|
|
601
|
-
<button class="danger" id="stopBtn" type="button" data-i18n="
|
|
689
|
+
<button class="danger" id="stopBtn" type="button" data-i18n="handoff.detach">Pause Web attachment</button>
|
|
602
690
|
</div>
|
|
603
691
|
</div>
|
|
692
|
+
${handoffBarHtml()}
|
|
693
|
+
${terminalFindHtml()}
|
|
604
694
|
<div id="terminal" style="min-height:0;flex:1"></div>
|
|
695
|
+
${codexPanelHtml()}
|
|
696
|
+
${nativePanelHtml()}
|
|
697
|
+
${terminalComposerHtml()}
|
|
605
698
|
<div id="detectedPanel" style="display:none;overflow:auto;flex:1"></div>
|
|
606
699
|
</main>
|
|
700
|
+
${workspaceSecondaryHtml()}
|
|
701
|
+
</div>
|
|
702
|
+
</section>
|
|
607
703
|
|
|
608
704
|
<section class="action-result" id="actionResult" hidden aria-live="polite">
|
|
609
705
|
<header>
|
|
610
706
|
<strong id="actionResultTitle"></strong>
|
|
611
|
-
<button id="actionResultClose" type="button" aria-label="Close">×</button>
|
|
707
|
+
<button id="actionResultClose" type="button" data-i18n-aria="close" aria-label="Close">×</button>
|
|
612
708
|
</header>
|
|
613
709
|
<pre id="actionResultBody"></pre>
|
|
614
710
|
</section>
|
|
@@ -618,16 +714,54 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
618
714
|
<strong data-i18n="projectState">Project State</strong>
|
|
619
715
|
<button id="refreshBtn" type="button" data-i18n="refresh">Refresh</button>
|
|
620
716
|
</div>
|
|
717
|
+
${workbenchControlsHtml()}
|
|
621
718
|
<div class="state" id="state"></div>
|
|
622
719
|
</aside>
|
|
623
720
|
|
|
624
721
|
<button class="edge-toggle edge-left" id="toggleLeft" type="button" data-i18n-title="collapseSidebar" data-i18n-aria="toggleLeftSidebar" title="Collapse sidebar" aria-label="Toggle left sidebar">⟨</button>
|
|
625
722
|
<button class="edge-toggle edge-right" id="toggleRight" type="button" data-i18n-title="collapseStatePanel" data-i18n-aria="toggleRightPanel" title="Collapse state panel" aria-label="Toggle right panel">⟩</button>
|
|
626
|
-
<div class="edge-resizer edge-resizer-left" id="resizeLeft" role="separator" aria-orientation="vertical" data-i18n-aria="resizeLeftSidebar" data-i18n-title="resizeLeftSidebar" aria-label="Resize left sidebar" title="Resize left sidebar"></div>
|
|
627
|
-
<div class="edge-resizer edge-resizer-right" id="resizeRight" role="separator" aria-orientation="vertical" data-i18n-aria="resizeRightPanel" data-i18n-title="resizeRightPanel" aria-label="Resize right panel" title="Resize right panel"></div>
|
|
723
|
+
<div class="edge-resizer edge-resizer-left" id="resizeLeft" tabindex="0" role="separator" aria-orientation="vertical" data-i18n-aria="resizeLeftSidebar" data-i18n-title="resizeLeftSidebar" aria-label="Resize left sidebar" title="Resize left sidebar"></div>
|
|
724
|
+
<div class="edge-resizer edge-resizer-right" id="resizeRight" tabindex="0" role="separator" aria-orientation="vertical" data-i18n-aria="resizeRightPanel" data-i18n-title="resizeRightPanel" aria-label="Resize right panel" title="Resize right panel"></div>
|
|
628
725
|
</div>
|
|
629
726
|
|
|
630
|
-
<div class="dialog-overlay" id="
|
|
727
|
+
<div class="dialog-overlay" id="projectDialog" role="dialog" aria-modal="true" aria-labelledby="projectDialogTitle" hidden>
|
|
728
|
+
<div class="dialog">
|
|
729
|
+
<header class="dialog-heading"><h3 id="projectDialogTitle" data-i18n="addProject">Add project</h3><button id="projectDialogClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
|
|
730
|
+
<p class="dialog-help" data-i18n="projectDialogHelp">Register a local project directory to view its sessions and shared state.</p>
|
|
731
|
+
<p class="dialog-help" id="projectDialogError" role="alert" hidden></p>
|
|
732
|
+
<form id="projectForm" class="form">
|
|
733
|
+
<label><span data-i18n="projectPath">Project path</span><input id="projectPath" data-i18n-placeholder="projectPathPlaceholder" placeholder="/path/to/project" required autocomplete="off"></label>
|
|
734
|
+
<div class="btns"><button id="projectCancelBtn" type="button" data-i18n="dialog.cancel">Cancel</button><button id="addProjectBtn" class="primary" type="submit" data-i18n="registerProject">Register Project</button></div>
|
|
735
|
+
</form>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
<div class="dialog-overlay" id="startDialog" role="dialog" aria-modal="true" aria-labelledby="startDialogTitle" hidden>
|
|
739
|
+
<div class="dialog">
|
|
740
|
+
<header class="dialog-heading"><h3 id="startDialogTitle" data-i18n="newSession">New session</h3><button id="startDialogClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
|
|
741
|
+
<p class="dialog-help" data-i18n="startDialogHelp">Choose a CLI and start a new session or resume existing history.</p>
|
|
742
|
+
<p class="dialog-help" id="startDialogError" role="alert" hidden></p>
|
|
743
|
+
<div class="dialog-target"><span data-i18n="startProject">Target project</span>: <span id="startProjectPath"></span></div>
|
|
744
|
+
<form class="form" id="startForm">
|
|
745
|
+
<label><span data-i18n="kind">Kind</span><select id="kind"><option value="codex">codex</option><option value="claude">claude</option><option value="shell" data-i18n="kind.shell">shell</option></select></label>
|
|
746
|
+
<label id="transportField"><span data-i18n="handoff.transport">Codex interface</span><select id="transport"><option value="tmux" data-i18n="handoff.cli">Terminal CLI</option><option value="app-server" data-i18n="handoff.appServer">Structured App Server (opt in)</option></select></label>
|
|
747
|
+
<div class="start-options">
|
|
748
|
+
<label><span data-i18n="mode">Mode</span><select id="startMode"><option value="new" data-i18n="mode.new">new</option><option value="resume" data-i18n="mode.resume">resume</option><option value="last" data-i18n="mode.last">last</option><option value="continue" data-i18n="mode.continue">continue</option></select></label>
|
|
749
|
+
<label data-resume-field><span data-i18n="session">Session</span><select id="resumeSelect"></select></label>
|
|
750
|
+
<label data-resume-field data-resume-custom style="display:none"><span data-i18n="sessionId">Session id</span><input id="resumeArg" data-i18n-placeholder="sessionIdPlaceholder" placeholder="session id or name"></label>
|
|
751
|
+
</div>
|
|
752
|
+
<label id="appServerResumeField" hidden><span style="display:flex;align-items:flex-start;gap:8px"><input type="checkbox" id="appServerResumeConfirm"><span data-i18n="handoff.resumeConfirm">I have stopped the original executor. Resuming history starts a separate executor.</span></span></label>
|
|
753
|
+
<p class="dialog-help" data-i18n="dshLaunchHelp">For DeepSeek Harness, run hcc dsh web in the project. Its agents appear under Detected.</p>
|
|
754
|
+
<div class="btns"><button id="startCancelBtn" type="button" data-i18n="dialog.cancel">Cancel</button><button class="primary" type="submit" data-i18n="start">Start</button></div>
|
|
755
|
+
</form>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
<div class="dialog-overlay" id="settingsDialog" role="dialog" aria-modal="true" aria-labelledby="settingsTitle" hidden>
|
|
759
|
+
<div class="dialog settings-dialog">
|
|
760
|
+
<header><h3 id="settingsTitle" data-i18n="settings">Settings</h3><button id="settingsClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
|
|
761
|
+
<div class="settings-fields">${preferenceControlsHtml({ terminal: true })}</div>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="dialog-overlay" id="stopDialog" role="dialog" aria-modal="true" aria-labelledby="stopDialogTitle" hidden>
|
|
631
765
|
<div class="dialog">
|
|
632
766
|
<h3 id="stopDialogTitle">Stop session?</h3>
|
|
633
767
|
<div class="path" id="stopDialogMeta" style="font-size:12px"></div>
|
|
@@ -639,10 +773,21 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
639
773
|
</div>
|
|
640
774
|
</div>
|
|
641
775
|
|
|
776
|
+
${codexHistoryHtml()}
|
|
777
|
+
${reviewPanelHtml()}
|
|
778
|
+
${commandPaletteHtml()}
|
|
642
779
|
<script src="/assets/xterm.js"></script>
|
|
780
|
+
<script src="/assets/addon-fit.js"></script>
|
|
781
|
+
<script src="/assets/addon-search.js"></script>
|
|
643
782
|
<script${inlineScriptNonce}>
|
|
783
|
+
${uiHandoffScript()}
|
|
784
|
+
${workbenchScript()}
|
|
785
|
+
const hccUi = window.hccUi;
|
|
786
|
+
const paneMode = ${pane ? 'true' : 'false'};
|
|
787
|
+
const handoffStore = window.hccHandoffStore;
|
|
644
788
|
const initialParams = new URLSearchParams(location.search);
|
|
645
|
-
|
|
789
|
+
let loggedOut = false;
|
|
790
|
+
try { sessionStorage.removeItem('hcc_logged_out'); } catch {}
|
|
646
791
|
const token = initialParams.get('token') || '';
|
|
647
792
|
const runtimeApiVersion = ${API_VERSION};
|
|
648
793
|
const headers = {
|
|
@@ -651,23 +796,34 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
651
796
|
};
|
|
652
797
|
let currentProject = initialParams.get('project') || initialParams.get('root') || '';
|
|
653
798
|
let projects = [];
|
|
799
|
+
let startPending = false;
|
|
654
800
|
let sessionKindFilter = initialParams.get('kind') || 'all';
|
|
801
|
+
let sessionSearchQuery = '';
|
|
802
|
+
let sessionStatusFilter = 'all';
|
|
655
803
|
let sessions = []; // managed (PTY) sessions
|
|
804
|
+
let sessionsLoaded = false;
|
|
656
805
|
let detected = []; // coordination-only peers (from hooks/watcher)
|
|
657
806
|
let resumableCache = []; // provider sessions available to resume (from /api/resumable)
|
|
658
807
|
// The current token belongs to one terminal WebSocket connection and is
|
|
659
808
|
// replaced on every reconnect.
|
|
660
809
|
const sessionActionTokens = new Map();
|
|
661
|
-
const
|
|
810
|
+
const sessionControls = new Map();
|
|
811
|
+
let activeConnectionState = 'offline';
|
|
812
|
+
let activeLocalClients = null;
|
|
813
|
+
let draftReceipt = false;
|
|
814
|
+
let lastControlNotice = 0;
|
|
662
815
|
let active = null; // active managed session id
|
|
663
816
|
let activeDetected = null; // active detected peer id
|
|
664
817
|
let activeType = 'managed'; // 'managed' | 'detected'
|
|
665
|
-
let showStaleDetected =
|
|
818
|
+
let showStaleDetected = hccUi.safeGet('hcc.showStaleDetected') === '1';
|
|
666
819
|
let activePeerTtl = 600;
|
|
667
820
|
let lastStateNow = 0;
|
|
668
821
|
let lastStateRoot = '';
|
|
822
|
+
let lastStateData = null;
|
|
823
|
+
let lastActionResult = null;
|
|
669
824
|
let ws = null;
|
|
670
825
|
let wsReconnectTimer = null;
|
|
826
|
+
let lastSentTerminalSize = null;
|
|
671
827
|
// ui-4: after logout (or repeated failed reconnects) the WS must not loop
|
|
672
828
|
// forever every 2s against a server that now rejects the upgrade.
|
|
673
829
|
let wsReconnectFailures = 0;
|
|
@@ -677,261 +833,10 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
677
833
|
let terminalLastReplaceAt = 0;
|
|
678
834
|
let autoPollInFlight = false;
|
|
679
835
|
let projectPollInFlight = false;
|
|
680
|
-
|
|
681
|
-
en: {
|
|
682
|
-
language: 'Language',
|
|
683
|
-
project: 'Project',
|
|
684
|
-
projectPath: 'Project path',
|
|
685
|
-
projectPathPlaceholder: '/path/to/project',
|
|
686
|
-
registerProject: 'Register Project',
|
|
687
|
-
newSession: 'New session',
|
|
688
|
-
start: 'Start',
|
|
689
|
-
mode: 'Mode',
|
|
690
|
-
'mode.new': 'new',
|
|
691
|
-
'mode.resume': 'resume',
|
|
692
|
-
'mode.last': 'last',
|
|
693
|
-
'mode.continue': 'continue',
|
|
694
|
-
session: 'Session',
|
|
695
|
-
sessionId: 'Session id',
|
|
696
|
-
sessionIdPlaceholder: 'session id or name',
|
|
697
|
-
sessions: 'Sessions',
|
|
698
|
-
view: 'View',
|
|
699
|
-
'kind.all': 'all',
|
|
700
|
-
'kind.shell': 'shell',
|
|
701
|
-
'kind.other': 'other',
|
|
702
|
-
noSessionSelected: 'No session selected',
|
|
703
|
-
startOrSelect: 'Start or select a session from the left panel',
|
|
704
|
-
actions: 'Actions',
|
|
705
|
-
'action.state': 'state',
|
|
706
|
-
'action.inbox': 'inbox',
|
|
707
|
-
'action.claimNextTask': 'claim next task',
|
|
708
|
-
'action.status': 'status',
|
|
709
|
-
'action.renewHeartbeat': 'renew heartbeat',
|
|
710
|
-
'action.reregister': 're-register peer',
|
|
711
|
-
'action.runStatusTerminal': 'run status in terminal',
|
|
712
|
-
logout: 'log out',
|
|
713
|
-
actionResult: 'Action Result',
|
|
714
|
-
stop: 'stop',
|
|
715
|
-
projectState: 'Project State',
|
|
716
|
-
refresh: 'Refresh',
|
|
717
|
-
managed: 'Managed',
|
|
718
|
-
detected: 'Detected',
|
|
719
|
-
running: 'running',
|
|
720
|
-
noActiveSessions: 'No active sessions.',
|
|
721
|
-
startOneAbove: 'Start one above',
|
|
722
|
-
orRunTerminal: 'or run in any terminal:',
|
|
723
|
-
noDetectedPeers: 'No detected peers.',
|
|
724
|
-
viewFilter: 'View filter',
|
|
725
|
-
noTasks: 'No tasks.',
|
|
726
|
-
noPeers: 'No peers.',
|
|
727
|
-
noActiveLocks: 'No active locks.',
|
|
728
|
-
noMessages: 'No messages.',
|
|
729
|
-
noTimelineItems: 'No timeline items.',
|
|
730
|
-
nextAction: 'Next Action',
|
|
731
|
-
timeline: 'Timeline',
|
|
732
|
-
messages: 'Messages',
|
|
733
|
-
peers: 'Peers',
|
|
734
|
-
tasks: 'Tasks',
|
|
735
|
-
locks: 'Locks',
|
|
736
|
-
owner: 'owner',
|
|
737
|
-
assignee: 'assignee',
|
|
738
|
-
task: 'task',
|
|
739
|
-
all: 'all',
|
|
740
|
-
next: 'next',
|
|
741
|
-
finish: 'finish',
|
|
742
|
-
warnings: 'warnings',
|
|
743
|
-
noImmediateAction: 'No immediate coordination action',
|
|
744
|
-
detectedSession: 'Detected Session',
|
|
745
|
-
sendMessage: 'Send Message',
|
|
746
|
-
messageHelp: "Message will appear in the peer's <code>hcc msg inbox</code> and be injected on next hook fire.",
|
|
747
|
-
messageBodyPlaceholder: 'Message body...',
|
|
748
|
-
send: 'Send',
|
|
749
|
-
peer: 'peer',
|
|
750
|
-
kind: 'kind',
|
|
751
|
-
status: 'status',
|
|
752
|
-
cwd: 'cwd',
|
|
753
|
-
pid: 'pid',
|
|
754
|
-
unknown: 'unknown',
|
|
755
|
-
lastSeen: 'last seen',
|
|
756
|
-
secondsAgo: 's ago',
|
|
757
|
-
age: 'age',
|
|
758
|
-
branch: 'branch',
|
|
759
|
-
runtime: 'runtime',
|
|
760
|
-
command: 'command',
|
|
761
|
-
providerSession: 'provider session',
|
|
762
|
-
activeDetected: 'Active detected',
|
|
763
|
-
staleDetected: 'Stale detected',
|
|
764
|
-
noActiveDetectedPeers: 'No active detected peers.',
|
|
765
|
-
showStale: 'show stale',
|
|
766
|
-
hideStale: 'hide stale',
|
|
767
|
-
thread: 'thread',
|
|
768
|
-
reply: 'reply',
|
|
769
|
-
'status.active': 'active',
|
|
770
|
-
'status.stale': 'stale',
|
|
771
|
-
'status.unknown': 'unknown',
|
|
772
|
-
'status.running': 'running',
|
|
773
|
-
'status.idle': 'idle',
|
|
774
|
-
'status.exited': 'exited',
|
|
775
|
-
'status.claimed': 'claimed',
|
|
776
|
-
'status.review': 'review',
|
|
777
|
-
'status.blocked': 'blocked',
|
|
778
|
-
'status.done': 'done',
|
|
779
|
-
'status.abandoned': 'abandoned',
|
|
780
|
-
'status.pending': 'pending',
|
|
781
|
-
'conn.offline': 'offline',
|
|
782
|
-
'conn.online': 'online',
|
|
783
|
-
'conn.attached': 'attached',
|
|
784
|
-
'conn.reconnecting': 'reconnecting...',
|
|
785
|
-
'conn.coordinationOnly': 'coordination only',
|
|
786
|
-
'conn.error': 'error',
|
|
787
|
-
show: 'Show',
|
|
788
|
-
collapse: 'Collapse',
|
|
789
|
-
sidebar: 'sidebar',
|
|
790
|
-
statePanel: 'state panel',
|
|
791
|
-
dragToResize: 'drag to resize',
|
|
792
|
-
collapseSidebar: 'Collapse sidebar',
|
|
793
|
-
collapseStatePanel: 'Collapse state panel',
|
|
794
|
-
toggleLeftSidebar: 'Toggle left sidebar',
|
|
795
|
-
toggleRightPanel: 'Toggle right panel',
|
|
796
|
-
resizeLeftSidebar: 'Resize left sidebar',
|
|
797
|
-
resizeRightPanel: 'Resize right panel',
|
|
798
|
-
customSession: 'custom session id...',
|
|
799
|
-
'dialog.cancel': 'Cancel',
|
|
800
|
-
'dialog.killTmux': 'Also kill tmux session',
|
|
801
|
-
'action.stopPeer': 'Stop peer',
|
|
802
|
-
'action.restartPeer': 'Restart peer',
|
|
803
|
-
detectedPeer: 'detected peer'
|
|
804
|
-
},
|
|
805
|
-
zh: {
|
|
806
|
-
language: '语言',
|
|
807
|
-
project: '项目',
|
|
808
|
-
projectPath: '项目路径',
|
|
809
|
-
projectPathPlaceholder: '/项目/路径',
|
|
810
|
-
registerProject: '注册项目',
|
|
811
|
-
newSession: '新会话',
|
|
812
|
-
start: '启动',
|
|
813
|
-
mode: '模式',
|
|
814
|
-
'mode.new': '新建',
|
|
815
|
-
'mode.resume': '恢复',
|
|
816
|
-
'mode.last': '最近',
|
|
817
|
-
'mode.continue': '继续',
|
|
818
|
-
session: '会话',
|
|
819
|
-
sessionId: '会话 ID',
|
|
820
|
-
sessionIdPlaceholder: '会话 ID 或名称',
|
|
821
|
-
sessions: '会话',
|
|
822
|
-
view: '视图',
|
|
823
|
-
'kind.all': '全部',
|
|
824
|
-
'kind.shell': 'shell',
|
|
825
|
-
'kind.other': '其他',
|
|
826
|
-
noSessionSelected: '未选择会话',
|
|
827
|
-
startOrSelect: '从左侧面板启动或选择一个会话',
|
|
828
|
-
actions: '操作',
|
|
829
|
-
'action.state': '状态详情',
|
|
830
|
-
'action.inbox': '收件箱',
|
|
831
|
-
'action.claimNextTask': '认领下个任务',
|
|
832
|
-
'action.status': '状态',
|
|
833
|
-
'action.renewHeartbeat': '续期心跳',
|
|
834
|
-
'action.reregister': '重新注册协作方',
|
|
835
|
-
'action.runStatusTerminal': '在终端运行状态命令',
|
|
836
|
-
logout: '退出登录',
|
|
837
|
-
actionResult: '操作结果',
|
|
838
|
-
stop: '停止',
|
|
839
|
-
projectState: '项目状态',
|
|
840
|
-
refresh: '刷新',
|
|
841
|
-
managed: '托管',
|
|
842
|
-
detected: '检测到',
|
|
843
|
-
running: '运行中',
|
|
844
|
-
noActiveSessions: '没有活动会话。',
|
|
845
|
-
startOneAbove: '在上方启动一个会话',
|
|
846
|
-
orRunTerminal: '或在任意终端运行:',
|
|
847
|
-
noDetectedPeers: '没有检测到的协作方。',
|
|
848
|
-
viewFilter: '视图过滤',
|
|
849
|
-
noTasks: '没有任务。',
|
|
850
|
-
noPeers: '没有协作方。',
|
|
851
|
-
noActiveLocks: '没有活动锁。',
|
|
852
|
-
noMessages: '没有消息。',
|
|
853
|
-
noTimelineItems: '没有时间线条目。',
|
|
854
|
-
nextAction: '下一步',
|
|
855
|
-
timeline: '时间线',
|
|
856
|
-
messages: '消息',
|
|
857
|
-
peers: '协作方',
|
|
858
|
-
tasks: '任务',
|
|
859
|
-
locks: '锁',
|
|
860
|
-
owner: '负责人',
|
|
861
|
-
assignee: '执行者',
|
|
862
|
-
task: '任务',
|
|
863
|
-
all: '全部',
|
|
864
|
-
next: '下一步',
|
|
865
|
-
finish: '收尾',
|
|
866
|
-
warnings: '警告',
|
|
867
|
-
noImmediateAction: '当前没有需要立即处理的协作动作',
|
|
868
|
-
detectedSession: '检测到的会话',
|
|
869
|
-
sendMessage: '发送消息',
|
|
870
|
-
messageHelp: '消息会出现在该 peer 的 <code>hcc msg inbox</code> 中,并在下一次 hook 触发时注入。',
|
|
871
|
-
messageBodyPlaceholder: '消息内容...',
|
|
872
|
-
send: '发送',
|
|
873
|
-
peer: '协作方',
|
|
874
|
-
kind: '类型',
|
|
875
|
-
status: '状态',
|
|
876
|
-
cwd: '工作目录',
|
|
877
|
-
pid: 'PID',
|
|
878
|
-
unknown: '未知',
|
|
879
|
-
lastSeen: '最后出现',
|
|
880
|
-
secondsAgo: ' 秒前',
|
|
881
|
-
age: '时长',
|
|
882
|
-
branch: '分支',
|
|
883
|
-
runtime: '运行时',
|
|
884
|
-
command: '命令',
|
|
885
|
-
providerSession: '提供方会话',
|
|
886
|
-
activeDetected: '活跃检测',
|
|
887
|
-
staleDetected: '过期检测',
|
|
888
|
-
noActiveDetectedPeers: '没有活跃的检测协作方。',
|
|
889
|
-
showStale: '显示过期',
|
|
890
|
-
hideStale: '隐藏过期',
|
|
891
|
-
thread: '线程',
|
|
892
|
-
reply: '回复',
|
|
893
|
-
'status.active': '活跃',
|
|
894
|
-
'status.stale': '过期',
|
|
895
|
-
'status.unknown': '未知',
|
|
896
|
-
'status.running': '运行中',
|
|
897
|
-
'status.idle': '空闲',
|
|
898
|
-
'status.exited': '已退出',
|
|
899
|
-
'status.claimed': '已认领',
|
|
900
|
-
'status.review': '审查中',
|
|
901
|
-
'status.blocked': '阻塞',
|
|
902
|
-
'status.done': '完成',
|
|
903
|
-
'status.abandoned': '已放弃',
|
|
904
|
-
'status.pending': '待处理',
|
|
905
|
-
'conn.offline': '离线',
|
|
906
|
-
'conn.online': '在线',
|
|
907
|
-
'conn.attached': '已连接',
|
|
908
|
-
'conn.reconnecting': '重连中...',
|
|
909
|
-
'conn.coordinationOnly': '仅协作',
|
|
910
|
-
'conn.error': '错误',
|
|
911
|
-
show: '显示',
|
|
912
|
-
collapse: '折叠',
|
|
913
|
-
sidebar: '侧栏',
|
|
914
|
-
statePanel: '状态面板',
|
|
915
|
-
dragToResize: '拖动调整大小',
|
|
916
|
-
collapseSidebar: '折叠侧栏',
|
|
917
|
-
collapseStatePanel: '折叠状态面板',
|
|
918
|
-
toggleLeftSidebar: '切换左侧栏',
|
|
919
|
-
toggleRightPanel: '切换右侧面板',
|
|
920
|
-
resizeLeftSidebar: '调整左侧栏宽度',
|
|
921
|
-
resizeRightPanel: '调整右侧面板宽度',
|
|
922
|
-
customSession: '自定义会话 ID...',
|
|
923
|
-
'dialog.cancel': '取消',
|
|
924
|
-
'dialog.killTmux': '同时终止 tmux 会话',
|
|
925
|
-
'action.stopPeer': '停止协作方',
|
|
926
|
-
'action.restartPeer': '恢复协作方',
|
|
927
|
-
detectedPeer: '检测到的协作方'
|
|
928
|
-
}
|
|
929
|
-
};
|
|
930
|
-
let lang = localStorage.getItem('hcc.lang') || ((navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en');
|
|
931
|
-
if (!i18n[lang]) lang = 'en';
|
|
836
|
+
let lang = hccUi.language;
|
|
932
837
|
|
|
933
838
|
function tr(key, fallback = '') {
|
|
934
|
-
return
|
|
839
|
+
return hccUi.tr(key, fallback);
|
|
935
840
|
}
|
|
936
841
|
|
|
937
842
|
function setText(id, key) {
|
|
@@ -948,14 +853,9 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
948
853
|
}
|
|
949
854
|
|
|
950
855
|
function applyLanguage() {
|
|
856
|
+
lang = hccUi.language;
|
|
951
857
|
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
|
952
|
-
|
|
953
|
-
const sel = document.getElementById('langSelect');
|
|
954
|
-
if (sel) sel.value = lang;
|
|
955
|
-
document.querySelectorAll('[data-i18n]').forEach((el) => { el.textContent = tr(el.dataset.i18n); });
|
|
956
|
-
document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { el.placeholder = tr(el.dataset.i18nPlaceholder); });
|
|
957
|
-
document.querySelectorAll('[data-i18n-title]').forEach((el) => { el.title = tr(el.dataset.i18nTitle); });
|
|
958
|
-
document.querySelectorAll('[data-i18n-aria]').forEach((el) => { el.setAttribute('aria-label', tr(el.dataset.i18nAria)); });
|
|
858
|
+
hccUi.translate();
|
|
959
859
|
if (!active && !activeDetected) {
|
|
960
860
|
setText('activeTitle', 'noSessionSelected');
|
|
961
861
|
setText('activeMeta', 'startOrSelect');
|
|
@@ -964,6 +864,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
964
864
|
connText(stateKey);
|
|
965
865
|
syncStartModeOptions();
|
|
966
866
|
renderSections();
|
|
867
|
+
renderHandoff();
|
|
967
868
|
if (activeType === 'detected' && activeDetected) {
|
|
968
869
|
const draft = document.getElementById('detMsg')?.value || '';
|
|
969
870
|
const peer = detected.find((p) => p.id === activeDetected) || { id: activeDetected };
|
|
@@ -974,11 +875,13 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
974
875
|
} else if (activeType === 'managed' && active) {
|
|
975
876
|
const meta = sessions.find((s) => s.id === active);
|
|
976
877
|
if (meta) {
|
|
977
|
-
|
|
978
|
-
document.getElementById('activeMeta').textContent = sessionMetaText(meta);
|
|
878
|
+
renderActiveSession(meta);
|
|
979
879
|
}
|
|
980
880
|
}
|
|
981
|
-
if (
|
|
881
|
+
if (lastStateData) renderState(lastStateData);
|
|
882
|
+
if (lastActionResult && !document.getElementById('actionResult').hidden) showActionResult(lastActionResult);
|
|
883
|
+
if (!stopDialog.hidden) updateStopDialogTitle();
|
|
884
|
+
syncFocusButton();
|
|
982
885
|
syncToggleIcons();
|
|
983
886
|
}
|
|
984
887
|
|
|
@@ -997,80 +900,266 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
997
900
|
const params = new URLSearchParams(location.search);
|
|
998
901
|
if (token) params.set('token', token);
|
|
999
902
|
if (currentProject) params.set('project', currentProject);
|
|
903
|
+
if (!paneMode) params.delete('session');
|
|
1000
904
|
if (sessionKindFilter && sessionKindFilter !== 'all') params.set('kind', sessionKindFilter);
|
|
1001
905
|
history.replaceState(null, '', location.pathname + '?' + params.toString());
|
|
1002
906
|
}
|
|
1003
907
|
|
|
1004
908
|
const term = new Terminal({
|
|
909
|
+
// Search match decorations use xterm's proposed decoration API.
|
|
910
|
+
allowProposedApi: true,
|
|
1005
911
|
cursorBlink: true,
|
|
1006
912
|
cursorInactiveStyle: 'outline',
|
|
1007
913
|
cursorStyle: 'bar',
|
|
1008
914
|
convertEol: true,
|
|
1009
915
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
1010
|
-
fontSize:
|
|
1011
|
-
|
|
916
|
+
fontSize: hccUi.preferences.fontSize,
|
|
917
|
+
lineHeight: 1.15,
|
|
918
|
+
theme: ${JSON.stringify(TERMINAL_THEMES)}[hccUi.preferences.terminalTheme === 'system' ? hccUi.theme : hccUi.preferences.terminalTheme]
|
|
1012
919
|
});
|
|
920
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
921
|
+
term.loadAddon(fitAddon);
|
|
922
|
+
const searchAddon = new SearchAddon.SearchAddon({ highlightLimit: 1000 });
|
|
923
|
+
term.loadAddon(searchAddon);
|
|
1013
924
|
term.open(document.getElementById('terminal'));
|
|
1014
925
|
|
|
1015
|
-
// Accurate terminal sizing via character measurement
|
|
1016
|
-
function measureCharSize() {
|
|
1017
|
-
const el = document.getElementById('terminal');
|
|
1018
|
-
const canvas = document.createElement('canvas');
|
|
1019
|
-
const ctx2d = canvas.getContext('2d');
|
|
1020
|
-
ctx2d.font = '13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
|
|
1021
|
-
const m = ctx2d.measureText('M');
|
|
1022
|
-
return { w: m.width || 8, h: 17 };
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
926
|
function resizeTerm() {
|
|
1026
927
|
const el = document.getElementById('terminal');
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
|
|
928
|
+
if (!el.getClientRects().length || el.clientWidth <= 16 || el.clientHeight <= 16) return;
|
|
929
|
+
const size = fitAddon.proposeDimensions();
|
|
930
|
+
if (!size || !Number.isFinite(size.cols) || !Number.isFinite(size.rows)) return;
|
|
931
|
+
const { cols, rows } = size;
|
|
1030
932
|
term.resize(cols, rows);
|
|
1031
933
|
const actionToken = sessionActionTokens.get(active) || '';
|
|
1032
|
-
if (ws && ws.readyState === WebSocket.OPEN && actionToken)
|
|
934
|
+
if (ws && ws.readyState === WebSocket.OPEN && actionToken && canControl() &&
|
|
935
|
+
(!lastSentTerminalSize || lastSentTerminalSize.socket !== ws || lastSentTerminalSize.cols !== cols || lastSentTerminalSize.rows !== rows)) {
|
|
936
|
+
lastSentTerminalSize = { socket: ws, cols, rows };
|
|
1033
937
|
terminalLastResizeAt = Date.now();
|
|
1034
|
-
ws.send(JSON.stringify({ type: 'resize', cols, rows, action_token: actionToken }));
|
|
938
|
+
ws.send(JSON.stringify({ type: 'resize', cols, rows, action_token: actionToken, epoch: controlEpoch() }));
|
|
1035
939
|
}
|
|
1036
940
|
}
|
|
1037
941
|
window.addEventListener('resize', resizeTerm);
|
|
942
|
+
new ResizeObserver(() => resizeTerm()).observe(document.getElementById('terminal'));
|
|
943
|
+
document.fonts?.ready.then(() => resizeTerm());
|
|
1038
944
|
setTimeout(resizeTerm, 80);
|
|
1039
945
|
|
|
946
|
+
function applyTerminalPreferences() {
|
|
947
|
+
const palette = ${JSON.stringify(TERMINAL_THEMES)};
|
|
948
|
+
const theme = hccUi.preferences.terminalTheme === 'system' ? hccUi.theme : hccUi.preferences.terminalTheme;
|
|
949
|
+
term.options.theme = palette[theme];
|
|
950
|
+
term.options.fontSize = hccUi.preferences.fontSize;
|
|
951
|
+
document.getElementById('terminal').style.backgroundColor = palette[theme].background;
|
|
952
|
+
requestAnimationFrame(() => resizeTerm());
|
|
953
|
+
}
|
|
954
|
+
|
|
1040
955
|
term.onData((data) => {
|
|
1041
956
|
sendTerminalInput(data);
|
|
1042
957
|
});
|
|
1043
958
|
|
|
1044
|
-
function sendTerminalInput(data) {
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
959
|
+
function sendTerminalInput(data, inputId = '') {
|
|
960
|
+
if (sessions.find(item => item.id === active)?.type === 'native' || sessions.find(item => item.id === active)?.type === 'app-server') return false;
|
|
961
|
+
if (!active || !ws || ws.readyState !== WebSocket.OPEN || !canControl()) {
|
|
962
|
+
if (Date.now() - lastControlNotice > 3000) {
|
|
963
|
+
lastControlNotice = Date.now();
|
|
964
|
+
document.getElementById('handoffStatus').textContent = tr('handoff.controlRequired');
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
1050
967
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
function flushPendingTerminalInput(id, socket) {
|
|
1057
|
-
const data = pendingTerminalInput.get(id) || '';
|
|
1058
|
-
const actionToken = sessionActionTokens.get(id) || '';
|
|
1059
|
-
if (!data || active !== id || ws !== socket || socket.readyState !== WebSocket.OPEN || !actionToken) return;
|
|
1060
|
-
pendingTerminalInput.delete(id);
|
|
1061
|
-
socket.send(JSON.stringify({ type: 'input', data, action_token: actionToken }));
|
|
968
|
+
const actionToken = sessionActionTokens.get(active) || '';
|
|
969
|
+
if (!actionToken) return false;
|
|
970
|
+
ws.send(JSON.stringify({ type: 'input', data, action_token: actionToken, epoch: controlEpoch(), ...(inputId ? { input_id: inputId } : {}) }));
|
|
971
|
+
return true;
|
|
1062
972
|
}
|
|
1063
973
|
|
|
1064
974
|
async function api(path, options = {}) {
|
|
1065
975
|
const res = await fetch(path + (path.includes('?') ? '&' : '?') + requestQuery().slice(1), {
|
|
1066
976
|
...options,
|
|
1067
977
|
headers: { 'Content-Type': 'application/json', ...headers, ...(options.headers || {}) }
|
|
978
|
+
}).catch((cause) => {
|
|
979
|
+
const error = new Error(tr('networkError'));
|
|
980
|
+
error.detail = cause.message;
|
|
981
|
+
throw error;
|
|
1068
982
|
});
|
|
1069
983
|
const json = await res.json();
|
|
1070
|
-
if (!res.ok)
|
|
984
|
+
if (!res.ok) {
|
|
985
|
+
const error = new Error(tr('error.' + json.error?.code, tr('requestFailed')));
|
|
986
|
+
error.code = json.error?.code;
|
|
987
|
+
error.detail = json.error?.message || json.message || error.message;
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
1071
990
|
return json;
|
|
1072
991
|
}
|
|
1073
992
|
|
|
993
|
+
function controlEpoch() { return Number(sessionControls.get(active)?.epoch || 0); }
|
|
994
|
+
function canControl() {
|
|
995
|
+
return Boolean(active && sessions.find((item) => item.id === active)?.status === 'running' && ws?.readyState === WebSocket.OPEN && sessionActionTokens.get(active) && sessionControls.get(active)?.can_control);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function renderActiveSession(meta) {
|
|
999
|
+
document.getElementById('activeTitle').textContent = sessionPeerId(meta) || active || '';
|
|
1000
|
+
document.getElementById('activeMeta').textContent = sessionMetaText(meta);
|
|
1001
|
+
const task = document.getElementById('activeTask');
|
|
1002
|
+
task.hidden = !meta?.task?.title;
|
|
1003
|
+
task.textContent = meta?.task?.title ? tr('handoff.task') + ' #' + meta.task.id + ': ' + meta.task.title + (meta.task.status ? ' · ' + statusText(meta.task.status) : '') : '';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function renderDraftState() {
|
|
1007
|
+
const value = active ? handoffStore.draft(currentProject, active) : { text: '', pending: null };
|
|
1008
|
+
const note = document.getElementById('draftStatus');
|
|
1009
|
+
note.textContent = tr(value.pending ? value.pending.status === 'uncertain' ? 'handoff.uncertain' : 'handoff.pending' : draftReceipt ? 'handoff.received' : 'handoff.saved');
|
|
1010
|
+
note.title = tr('handoff.storageHint');
|
|
1011
|
+
document.getElementById('reviewDraftBtn').hidden = value.pending?.status !== 'uncertain';
|
|
1012
|
+
document.getElementById('sendDraftBtn').disabled = !canControl() || !value.text || Boolean(value.pending);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function renderHandoff() {
|
|
1016
|
+
document.getElementById('handoffBar').hidden = !active;
|
|
1017
|
+
const control = sessionControls.get(active) || {};
|
|
1018
|
+
const writable = canControl();
|
|
1019
|
+
term.options.disableStdin = !writable;
|
|
1020
|
+
const copy = (en, zh) => lang === 'zh' ? zh : en;
|
|
1021
|
+
const connection = document.getElementById('handoffConnection');
|
|
1022
|
+
connection.textContent = copy('Web: ', '连接:') + (activeConnectionState === 'connected' ? copy('Connected', '已连接') : activeConnectionState === 'connecting' ? copy('Connecting…', '连接中…') : copy('Disconnected', '已断开'));
|
|
1023
|
+
connection.dataset.tone = activeConnectionState === 'connected' ? 'ok' : 'warn';
|
|
1024
|
+
const meta = sessions.find((item) => item.id === active);
|
|
1025
|
+
const processState = document.getElementById('handoffProcess');
|
|
1026
|
+
processState.textContent = copy('Process: ', '进程:') + statusText(meta?.status || 'unknown');
|
|
1027
|
+
processState.dataset.tone = meta?.status === 'running' ? 'ok' : meta?.status === 'exited' ? 'danger' : '';
|
|
1028
|
+
const access = document.getElementById('handoffAccess');
|
|
1029
|
+
access.textContent = writable ? copy('You control', '你正在控制') : control.has_controller ? control.controller_connected === false ? copy('Control retained · Read only', '控制权暂时保留 · 只读') : copy('Other window controls · Read only', '其他窗口控制 · 只读') : copy('Read only', '只读');
|
|
1030
|
+
access.dataset.tone = writable ? 'ok' : control.has_controller ? 'warn' : '';
|
|
1031
|
+
document.getElementById('handoffHelp').textContent = copy('Attachment details', '连接与离开说明');
|
|
1032
|
+
const claim = document.getElementById('claimControlBtn');
|
|
1033
|
+
claim.hidden = writable;
|
|
1034
|
+
claim.disabled = activeConnectionState !== 'connected' || !sessionActionTokens.get(active);
|
|
1035
|
+
claim.textContent = tr(control.has_controller ? 'handoff.takeover' : 'handoff.claim');
|
|
1036
|
+
document.getElementById('releaseControlBtn').hidden = !writable;
|
|
1037
|
+
const reconnect = document.getElementById('reconnectBtn');
|
|
1038
|
+
reconnect.disabled = !active || activeConnectionState === 'connecting';
|
|
1039
|
+
reconnect.hidden = activeConnectionState === 'connected';
|
|
1040
|
+
const clients = activeLocalClients ?? meta?.local_clients;
|
|
1041
|
+
const count = typeof clients === 'number' ? clients : Array.isArray(clients) ? clients.length : Number(clients?.count || 0);
|
|
1042
|
+
document.getElementById('handoffDetail').textContent = (meta?.type === 'tmux' ? (clients?.state === 'unknown' ? tr('handoff.localUnknown') : count ? tr('handoff.localClients') + ': ' + count : tr('handoff.noLocalClients')) + ' · ' + tr('handoff.localBoundary') + ' ' : '') + tr('handoff.leaveHelp');
|
|
1043
|
+
const stop = document.getElementById('stopBtn');
|
|
1044
|
+
stop.hidden = meta?.type === 'native';
|
|
1045
|
+
stop.disabled = !writable;
|
|
1046
|
+
stop.dataset.i18n = meta?.type === 'tmux' ? 'handoff.detach' : 'handoff.terminate';
|
|
1047
|
+
stop.textContent = tr(stop.dataset.i18n);
|
|
1048
|
+
document.querySelectorAll('[data-terminal-action]').forEach((button) => { button.disabled = !writable || meta?.type === 'app-server' || meta?.type === 'native'; });
|
|
1049
|
+
document.querySelectorAll('[data-action]').forEach((button) => {
|
|
1050
|
+
if (!['status', 'state', 'inbox'].includes(button.dataset.action)) button.disabled = !writable;
|
|
1051
|
+
});
|
|
1052
|
+
renderDraftState();
|
|
1053
|
+
window.hccCodex?.sync();
|
|
1054
|
+
window.hccNative?.sync();
|
|
1055
|
+
window.hccHistory?.sync();
|
|
1056
|
+
window.hccReview?.sync();
|
|
1057
|
+
window.hccTerminalFind?.sync();
|
|
1058
|
+
window.hccWorkspace?.sync();
|
|
1059
|
+
document.getElementById('openReviewDialog').disabled = !meta;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function disconnectWebSocket() {
|
|
1063
|
+
clearTimeout(wsReconnectTimer);
|
|
1064
|
+
const previous = ws;
|
|
1065
|
+
ws = null;
|
|
1066
|
+
if (active) {
|
|
1067
|
+
handoffStore.uncertain(currentProject, active);
|
|
1068
|
+
sessionActionTokens.delete(active);
|
|
1069
|
+
sessionControls.delete(active);
|
|
1070
|
+
}
|
|
1071
|
+
activeConnectionState = 'offline';
|
|
1072
|
+
lastSentTerminalSize = null;
|
|
1073
|
+
previous?.close();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function restoreSelection() {
|
|
1077
|
+
const explicit = initialParams.get('session');
|
|
1078
|
+
initialParams.delete('session');
|
|
1079
|
+
const selected = explicit ? { type: 'managed', id: explicit } : handoffStore.selected(currentProject);
|
|
1080
|
+
if (selected) {
|
|
1081
|
+
if (selected.type === 'managed' && sessions.some((item) => item.id === selected.id && item.status === 'running')) return connectManaged(selected.id);
|
|
1082
|
+
if (selected.type === 'detected' && detected.some((item) => item.id === selected.id)) return connectDetected(selected.id);
|
|
1083
|
+
document.getElementById('activeMeta').textContent = tr('handoff.missingSelection');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const first = sessions.find((item) => item.status === 'running');
|
|
1087
|
+
if (first) connectManaged(first.id);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
window.hccHandoff = {
|
|
1091
|
+
get active() { return active; },
|
|
1092
|
+
get projectRoot() { return currentProject; },
|
|
1093
|
+
get draftScope() { return window.hccDraftScope || ''; },
|
|
1094
|
+
get actionToken() { return sessionActionTokens.get(active) || ''; },
|
|
1095
|
+
get epoch() { return controlEpoch(); },
|
|
1096
|
+
get canControl() { return canControl(); },
|
|
1097
|
+
get session() { return sessions.find(item => item.id === active) || null; },
|
|
1098
|
+
get sessions() { return sessions; },
|
|
1099
|
+
refreshSessions: () => refreshSessions(), openManaged: id => connectManaged(id), openDialog, closeDialog,
|
|
1100
|
+
api: (...args) => api(...args), tr, esc
|
|
1101
|
+
};
|
|
1102
|
+
${codexPanelScript()}
|
|
1103
|
+
${codexHistoryScript()}
|
|
1104
|
+
${reviewPanelScript()}
|
|
1105
|
+
${nativePanelScript()}
|
|
1106
|
+
document.getElementById('claimControlBtn').addEventListener('click', () => {
|
|
1107
|
+
if (ws?.readyState !== WebSocket.OPEN) return;
|
|
1108
|
+
const target = active, socket = ws, epoch = controlEpoch();
|
|
1109
|
+
const takeover = Boolean(sessionControls.get(active)?.has_controller);
|
|
1110
|
+
if (takeover && !confirm(lang === 'zh' ? '接管后,其他浏览器窗口将转为只读。当前任务继续运行。是否接管?' : 'Taking control makes the other browser window read only. The current task keeps running. Continue?')) return;
|
|
1111
|
+
if (active !== target || ws !== socket) return;
|
|
1112
|
+
socket.send(JSON.stringify({ type: 'control', action: 'claim', action_token: sessionActionTokens.get(target) || '', epoch, force: takeover }));
|
|
1113
|
+
});
|
|
1114
|
+
document.getElementById('releaseControlBtn').addEventListener('click', () => {
|
|
1115
|
+
if (!canControl()) return;
|
|
1116
|
+
ws.send(JSON.stringify({ type: 'control', action: 'release', action_token: sessionActionTokens.get(active) || '', epoch: controlEpoch() }));
|
|
1117
|
+
});
|
|
1118
|
+
document.getElementById('reconnectBtn').addEventListener('click', () => {
|
|
1119
|
+
if (!active) return;
|
|
1120
|
+
const id = active;
|
|
1121
|
+
disconnectWebSocket();
|
|
1122
|
+
wsReconnectFailures = 0;
|
|
1123
|
+
openWs(id);
|
|
1124
|
+
});
|
|
1125
|
+
document.getElementById('terminalDraft').addEventListener('input', (event) => {
|
|
1126
|
+
if (!active) return;
|
|
1127
|
+
draftReceipt = false;
|
|
1128
|
+
handoffStore.saveDraft(currentProject, active, event.target.value);
|
|
1129
|
+
renderDraftState();
|
|
1130
|
+
});
|
|
1131
|
+
function submitTerminalDraft(event) {
|
|
1132
|
+
event.preventDefault();
|
|
1133
|
+
if (!active || !canControl()) return;
|
|
1134
|
+
const textarea = document.getElementById('terminalDraft');
|
|
1135
|
+
const text = textarea.value;
|
|
1136
|
+
if (!text || handoffStore.draft(currentProject, active).pending) return;
|
|
1137
|
+
const inputId = window.crypto?.randomUUID?.() || 'input-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
1138
|
+
handoffStore.saveDraft(currentProject, active, text);
|
|
1139
|
+
handoffStore.submit(currentProject, active, inputId, text);
|
|
1140
|
+
try {
|
|
1141
|
+
if (!sendTerminalInput(text + '\\r', inputId)) handoffStore.uncertain(currentProject, active);
|
|
1142
|
+
} catch {
|
|
1143
|
+
handoffStore.uncertain(currentProject, active);
|
|
1144
|
+
}
|
|
1145
|
+
renderDraftState();
|
|
1146
|
+
}
|
|
1147
|
+
document.getElementById('terminalComposer').addEventListener('submit', submitTerminalDraft);
|
|
1148
|
+
document.getElementById('terminalDraft').addEventListener('keydown', (event) => {
|
|
1149
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') submitTerminalDraft(event);
|
|
1150
|
+
});
|
|
1151
|
+
document.getElementById('reviewDraftBtn').addEventListener('click', () => {
|
|
1152
|
+
if (!active) return;
|
|
1153
|
+
handoffStore.review(currentProject, active);
|
|
1154
|
+
draftReceipt = false;
|
|
1155
|
+
renderDraftState();
|
|
1156
|
+
});
|
|
1157
|
+
window.addEventListener('pagehide', () => disconnectWebSocket());
|
|
1158
|
+
|
|
1159
|
+
function handleUiError(error, action = '') {
|
|
1160
|
+
showActionResult({ action, peer: activePeerInfo()?.peerId || '', error: error.detail || error.message, errorCode: error.code });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1074
1163
|
function renderProjects() {
|
|
1075
1164
|
const select = document.getElementById('projectSelect');
|
|
1076
1165
|
select.innerHTML = projects.map((p) =>
|
|
@@ -1078,25 +1167,30 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1078
1167
|
).join('');
|
|
1079
1168
|
if (currentProject) select.value = currentProject;
|
|
1080
1169
|
document.getElementById('sessionKindFilter').value = sessionKindFilter;
|
|
1170
|
+
window.hccCommandPalette?.refresh();
|
|
1081
1171
|
}
|
|
1082
1172
|
|
|
1083
1173
|
function kindMatches(item) {
|
|
1084
|
-
const kind = ['claude', 'codex', 'shell'].includes(item.kind) ? item.kind : 'other';
|
|
1174
|
+
const kind = ['claude', 'codex', 'dsh', 'shell'].includes(item.kind) ? item.kind : 'other';
|
|
1085
1175
|
return sessionKindFilter === 'all' || kind === sessionKindFilter;
|
|
1086
1176
|
}
|
|
1087
1177
|
|
|
1088
1178
|
function syncStartModeOptions() {
|
|
1089
1179
|
const kind = document.getElementById('kind').value;
|
|
1180
|
+
document.getElementById('transportField').hidden = kind !== 'codex';
|
|
1181
|
+
const appServer = kind === 'codex' && document.getElementById('transport').value === 'app-server';
|
|
1090
1182
|
const modeSelect = document.getElementById('startMode');
|
|
1091
1183
|
const current = modeSelect.value;
|
|
1092
1184
|
const modes = kind === 'claude'
|
|
1093
1185
|
? [['new', 'new'], ['resume', 'resume'], ['continue', 'continue']]
|
|
1094
1186
|
: kind === 'codex'
|
|
1095
|
-
? [['new', 'new'], ['resume', 'resume'], ['last', 'last']]
|
|
1187
|
+
? (appServer ? [['new', 'new'], ['resume', 'resume']] : [['new', 'new'], ['resume', 'resume'], ['last', 'last']])
|
|
1096
1188
|
: [['new', 'new']];
|
|
1097
1189
|
modeSelect.innerHTML = modes.map(([value]) => '<option value="' + value + '">' + tr('mode.' + value, value) + '</option>').join('');
|
|
1098
1190
|
modeSelect.value = modes.some(([value]) => value === current) ? current : 'new';
|
|
1099
1191
|
const isResume = modeSelect.value === 'resume';
|
|
1192
|
+
document.getElementById('appServerResumeField').hidden = !(appServer && isResume);
|
|
1193
|
+
document.getElementById('resumeArg').required = false;
|
|
1100
1194
|
document.querySelector('[data-resume-field]:not([data-resume-custom])').style.display = isResume ? '' : 'none';
|
|
1101
1195
|
if (isResume) loadResumable();
|
|
1102
1196
|
else document.querySelector('[data-resume-custom]').style.display = 'none';
|
|
@@ -1126,7 +1220,9 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1126
1220
|
}
|
|
1127
1221
|
function toggleResumeCustom() {
|
|
1128
1222
|
const sel = document.getElementById('resumeSelect');
|
|
1129
|
-
document.
|
|
1223
|
+
const custom = document.getElementById('startMode').value === 'resume' && sel.value === '__custom__';
|
|
1224
|
+
document.querySelector('[data-resume-custom]').style.display = custom ? '' : 'none';
|
|
1225
|
+
document.getElementById('resumeArg').required = custom;
|
|
1130
1226
|
}
|
|
1131
1227
|
|
|
1132
1228
|
async function loadProjects() {
|
|
@@ -1138,18 +1234,35 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1138
1234
|
}
|
|
1139
1235
|
|
|
1140
1236
|
async function switchProject(root) {
|
|
1237
|
+
if (paneMode && root !== currentProject) return;
|
|
1238
|
+
window.hccWorkspace?.reset();
|
|
1239
|
+
disconnectWebSocket();
|
|
1240
|
+
window.hccHistory?.reset();
|
|
1241
|
+
window.hccReview?.reset();
|
|
1141
1242
|
currentProject = root;
|
|
1142
1243
|
updateLocationProject();
|
|
1143
1244
|
active = null;
|
|
1144
1245
|
activeDetected = null;
|
|
1145
1246
|
activeType = 'managed';
|
|
1146
|
-
|
|
1247
|
+
sessions = [];
|
|
1248
|
+
sessionsLoaded = false;
|
|
1249
|
+
detected = [];
|
|
1250
|
+
renderSections();
|
|
1251
|
+
document.getElementById('rootPath').textContent = root;
|
|
1252
|
+
lastStateData = null;
|
|
1253
|
+
lastStateRoot = '';
|
|
1254
|
+
window.hccWorkbench.reset();
|
|
1147
1255
|
term.reset();
|
|
1256
|
+
document.getElementById('codexPanel').hidden = true;
|
|
1257
|
+
document.getElementById('nativePanel').hidden = true;
|
|
1258
|
+
document.getElementById('terminalComposer').hidden = true;
|
|
1259
|
+
document.getElementById('activeTask').hidden = true;
|
|
1260
|
+
renderHandoff();
|
|
1148
1261
|
document.getElementById('activeTitle').textContent = tr('noSessionSelected');
|
|
1149
1262
|
document.getElementById('activeMeta').textContent = tr('startOrSelect');
|
|
1150
1263
|
await Promise.all([refreshSessions(), refreshDetected(), refreshState()]);
|
|
1151
|
-
|
|
1152
|
-
|
|
1264
|
+
if (currentProject !== root) return;
|
|
1265
|
+
restoreSelection();
|
|
1153
1266
|
}
|
|
1154
1267
|
|
|
1155
1268
|
function esc(text) {
|
|
@@ -1158,7 +1271,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1158
1271
|
|
|
1159
1272
|
function fmtTime(ts) {
|
|
1160
1273
|
if (!ts) return '';
|
|
1161
|
-
return new Date(ts * 1000).toLocaleTimeString();
|
|
1274
|
+
return new Date(ts * 1000).toLocaleTimeString(lang === 'zh' ? 'zh-CN' : 'en');
|
|
1162
1275
|
}
|
|
1163
1276
|
|
|
1164
1277
|
function badgeClass(status) {
|
|
@@ -1224,6 +1337,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1224
1337
|
return [
|
|
1225
1338
|
runtimeTargetText(session),
|
|
1226
1339
|
sessionProviderSessionText(session),
|
|
1340
|
+
session?.capabilities?.structured_turns ? 'App Server' : '',
|
|
1227
1341
|
session?.command ? tr('command') + '=' + session.command : '',
|
|
1228
1342
|
session?.cwd || ''
|
|
1229
1343
|
].filter(Boolean).join(' · ');
|
|
@@ -1252,7 +1366,12 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1252
1366
|
return seen > 0 && (t - seen) <= activePeerTtl;
|
|
1253
1367
|
}
|
|
1254
1368
|
|
|
1369
|
+
function dshCoordinationPeer(peer) {
|
|
1370
|
+
return (peer?.provider || peer?.kind) === 'dsh' && ['hook', 'cordis'].includes(peer?.transport);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1255
1373
|
function detectedPeerCanStop(peer) {
|
|
1374
|
+
if (dshCoordinationPeer(peer)) return false;
|
|
1256
1375
|
const status = String(peer?.status || '').toLowerCase();
|
|
1257
1376
|
if (['exited', 'detached'].includes(status)) return false;
|
|
1258
1377
|
return peerIsActive(peer);
|
|
@@ -1284,11 +1403,11 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1284
1403
|
function taskOwnerStateText(task) {
|
|
1285
1404
|
if (!task?.owner) return '';
|
|
1286
1405
|
if (task.owner_stale) {
|
|
1287
|
-
if (task.takeover_ready) return '
|
|
1406
|
+
if (task.takeover_ready) return tr('ownerStaleNoLock');
|
|
1288
1407
|
const locks = Number(task.related_lock_count || 0);
|
|
1289
|
-
return locks ? '
|
|
1408
|
+
return locks ? tr('ownerStaleLocks') + ': ' + locks : tr('ownerStale');
|
|
1290
1409
|
}
|
|
1291
|
-
if (task.owner_active) return '
|
|
1410
|
+
if (task.owner_active) return tr('ownerActive');
|
|
1292
1411
|
return '';
|
|
1293
1412
|
}
|
|
1294
1413
|
|
|
@@ -1298,7 +1417,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1298
1417
|
}
|
|
1299
1418
|
|
|
1300
1419
|
function stateCardCollapsed(section) {
|
|
1301
|
-
return
|
|
1420
|
+
return hccUi.safeGet('hcc.stateCard.' + section + '.collapsed') === '1';
|
|
1302
1421
|
}
|
|
1303
1422
|
|
|
1304
1423
|
function stateCardHtml(section, title, count, bodyHtml) {
|
|
@@ -1322,7 +1441,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1322
1441
|
const collapsed = !card.classList.contains('state-card-collapsed');
|
|
1323
1442
|
card.classList.toggle('state-card-collapsed', collapsed);
|
|
1324
1443
|
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
1325
|
-
|
|
1444
|
+
hccUi.safeSet('hcc.stateCard.' + section + '.collapsed', collapsed ? '1' : '0');
|
|
1326
1445
|
});
|
|
1327
1446
|
});
|
|
1328
1447
|
}
|
|
@@ -1344,51 +1463,103 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1344
1463
|
}
|
|
1345
1464
|
|
|
1346
1465
|
document.getElementById('projectSelect').addEventListener('change', (event) => {
|
|
1347
|
-
switchProject(event.target.value).catch(
|
|
1348
|
-
});
|
|
1349
|
-
document.getElementById('langSelect').addEventListener('change', (event) => {
|
|
1350
|
-
lang = event.target.value === 'zh' ? 'zh' : 'en';
|
|
1351
|
-
applyLanguage();
|
|
1466
|
+
switchProject(event.target.value).catch(handleUiError);
|
|
1352
1467
|
});
|
|
1353
1468
|
document.getElementById('sessionKindFilter').addEventListener('change', (event) => {
|
|
1354
1469
|
sessionKindFilter = event.target.value || 'all';
|
|
1355
1470
|
updateLocationProject();
|
|
1356
1471
|
renderSections();
|
|
1357
1472
|
});
|
|
1358
|
-
document.getElementById('
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1473
|
+
document.getElementById('sessionSearch').addEventListener('input', (event) => {
|
|
1474
|
+
sessionSearchQuery = event.target.value;
|
|
1475
|
+
renderSections();
|
|
1476
|
+
});
|
|
1477
|
+
document.getElementById('sessionStatusFilter').addEventListener('change', (event) => {
|
|
1478
|
+
sessionStatusFilter = event.target.value || 'all';
|
|
1479
|
+
renderSections();
|
|
1480
|
+
});
|
|
1481
|
+
document.getElementById('projectForm').addEventListener('submit', async (event) => {
|
|
1482
|
+
event.preventDefault();
|
|
1483
|
+
const button = document.getElementById('addProjectBtn');
|
|
1484
|
+
if (button.disabled) return;
|
|
1485
|
+
button.disabled = true;
|
|
1486
|
+
document.getElementById('openProjectDialog').disabled = true;
|
|
1487
|
+
const errorElement = document.getElementById('projectDialogError');
|
|
1488
|
+
const originalProject = currentProject;
|
|
1489
|
+
errorElement.hidden = true;
|
|
1490
|
+
try {
|
|
1491
|
+
const input = document.getElementById('projectPath');
|
|
1492
|
+
const root = input.value.trim();
|
|
1493
|
+
if (!root) return;
|
|
1494
|
+
await api('/api/projects', { method: 'POST', body: JSON.stringify({ root }) });
|
|
1495
|
+
if (input.value.trim() === root) input.value = '';
|
|
1496
|
+
await loadProjects();
|
|
1497
|
+
if (projectDialog.hidden || currentProject !== originalProject) return;
|
|
1498
|
+
await switchProject(root);
|
|
1499
|
+
if (!projectDialog.hidden) closeDialog(projectDialog);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
errorElement.textContent = tr('error.' + error.code, error.detail || error.message);
|
|
1502
|
+
errorElement.hidden = false;
|
|
1503
|
+
} finally { button.disabled = false; document.getElementById('openProjectDialog').disabled = false; }
|
|
1366
1504
|
});
|
|
1367
1505
|
|
|
1506
|
+
function clearSessionFilters() {
|
|
1507
|
+
sessionSearchQuery = '';
|
|
1508
|
+
sessionStatusFilter = 'all';
|
|
1509
|
+
sessionKindFilter = 'all';
|
|
1510
|
+
document.getElementById('sessionSearch').value = '';
|
|
1511
|
+
document.getElementById('sessionStatusFilter').value = 'all';
|
|
1512
|
+
document.getElementById('sessionKindFilter').value = 'all';
|
|
1513
|
+
updateLocationProject();
|
|
1514
|
+
renderSections();
|
|
1515
|
+
document.getElementById('sessionSearch').focus();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function sessionMatchesSearch(item) {
|
|
1519
|
+
const words = sessionSearchQuery.trim().toLocaleLowerCase().split(/\\s+/).filter(Boolean);
|
|
1520
|
+
if (!words.length) return true;
|
|
1521
|
+
const searchable = [item.name, item.task?.title, item.id, item.peer_id, item.kind, item.provider, item.cwd, item.worktree, item.command,
|
|
1522
|
+
item.type, item.transport, item.branch, item.provider_session_label, item.binding?.runtime_target,
|
|
1523
|
+
item.binding?.provider, item.binding?.provider_session_id, item.binding?.provider_session_name]
|
|
1524
|
+
.filter(Boolean).join(' ').toLocaleLowerCase();
|
|
1525
|
+
return words.every((word) => searchable.includes(word));
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function sessionDetailsHtml(key, rows) {
|
|
1529
|
+
return '<details class="session-details" data-detail-key="' + esc(key) + '"><summary data-focus-key="detail:' + esc(key) + '">' + esc(tr('sessionDetails')) + '</summary><dl>' +
|
|
1530
|
+
rows.filter(([, value]) => value !== undefined && value !== null && value !== '').map(([label, value]) =>
|
|
1531
|
+
'<dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd>').join('') + '</dl></details>';
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1368
1534
|
// ── Sessions rendering (managed + detected) ──────────────────────────
|
|
1369
1535
|
function renderSections() {
|
|
1370
1536
|
const box = document.getElementById('sessions');
|
|
1371
|
-
const
|
|
1372
|
-
const
|
|
1537
|
+
const filtered = Boolean(sessionSearchQuery.trim() || sessionStatusFilter !== 'all' || sessionKindFilter !== 'all');
|
|
1538
|
+
const searching = Boolean(sessionSearchQuery.trim());
|
|
1539
|
+
const visibleSessions = sessions.filter((s) => kindMatches(s) && sessionMatchesSearch(s) &&
|
|
1540
|
+
(sessionStatusFilter === 'all' || s.status === 'running'))
|
|
1541
|
+
.sort((a, b) => Number(b.status === 'running') - Number(a.status === 'running') ||
|
|
1542
|
+
String(a.name || sessionPeerId(a)).localeCompare(String(b.name || sessionPeerId(b))));
|
|
1543
|
+
const visibleDetected = detected.filter((p) => kindMatches(p) && sessionMatchesSearch(p) &&
|
|
1544
|
+
(sessionStatusFilter === 'all' || peerIsActive(p)))
|
|
1545
|
+
.sort((a, b) => Number(b.last_seen_at || 0) - Number(a.last_seen_at || 0) || String(a.id).localeCompare(String(b.id)));
|
|
1373
1546
|
const activeDetectedPeers = visibleDetected.filter((p) => peerIsActive(p));
|
|
1374
1547
|
const staleDetectedPeers = visibleDetected.filter((p) => !peerIsActive(p));
|
|
1375
|
-
const
|
|
1376
|
-
const
|
|
1377
|
-
const text = String(value || '');
|
|
1378
|
-
return text.length > 14 ? text.slice(0, 10) + '...' : text;
|
|
1379
|
-
};
|
|
1548
|
+
const noMatches = '<div class="empty session-empty"><p>' + esc(tr('noMatchingSessions')) + '</p><button type="button" data-navigation="clear-session-filters">' + esc(tr('clearSessionFilters')) + '</button></div>';
|
|
1549
|
+
const startEmpty = '<div class="empty session-empty"><p>' + esc(tr('noActiveSessions')) + '</p><button class="primary" type="button" data-navigation="open-start">' + esc(tr('createFirstSession')) + '</button></div>';
|
|
1380
1550
|
const manHtml = visibleSessions.length
|
|
1381
1551
|
? visibleSessions.map((s) => {
|
|
1382
1552
|
const peerId = sessionPeerId(s);
|
|
1383
|
-
const
|
|
1553
|
+
const title = s.name || s.task?.title || peerId;
|
|
1554
|
+
const path = s.cwd || '';
|
|
1384
1555
|
return \`
|
|
1385
1556
|
<div class="session \${active === s.id && activeType === 'managed' ? 'active' : ''}" data-id="\${esc(s.id)}" data-type="managed">
|
|
1386
|
-
<div class="row"><strong>\${esc(
|
|
1387
|
-
<div class="
|
|
1388
|
-
|
|
1557
|
+
<div class="row"><button class="session-select session-title" type="button" data-focus-key="managed:\${esc(s.id)}" aria-label="\${esc(title)}" aria-pressed="\${active === s.id && activeType === 'managed'}" title="\${esc(title)}"><strong>\${esc(title)}</strong></button><span class="badge \${badgeClass(s.status)}">\${esc(statusText(s.status))}</span></div>
|
|
1558
|
+
<div class="session-subtitle"><span class="session-provider">\${esc(sessionProvider(s))}</span><span class="path" title="\${esc(path)}">\${esc(path.split('/').filter(Boolean).slice(-2).join('/'))}</span></div>
|
|
1559
|
+
\${sessionDetailsHtml('managed:' + s.id, [[tr('peer'), peerId], [tr('sessionId'), s.id], [tr('runtime'), sessionRuntimeTarget(s)], [tr('providerSession'), sessionProvider(s) + ':' + (sessionProviderSessionValue(s) || tr('unknown'))], [tr('handoff.transport'), s.type || 'pty'], [tr('command'), s.command], [tr('cwd'), path]])}
|
|
1389
1560
|
</div>\`;
|
|
1390
1561
|
}).join('')
|
|
1391
|
-
:
|
|
1562
|
+
: filtered ? '' : startEmpty;
|
|
1392
1563
|
|
|
1393
1564
|
const renderDetectedPeer = (p) => {
|
|
1394
1565
|
const state = peerStateView(p);
|
|
@@ -1396,52 +1567,65 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1396
1567
|
return \`
|
|
1397
1568
|
<div class="session \${activeDetected === p.id && activeType === 'detected' ? 'active' : ''}" data-id="\${esc(p.id)}" data-type="detected">
|
|
1398
1569
|
<div class="row">
|
|
1399
|
-
<strong>\${esc(p.id)}</strong>
|
|
1570
|
+
<button class="session-select session-title" type="button" data-focus-key="detected:\${esc(p.id)}" aria-label="\${esc(p.name || p.id)}" aria-pressed="\${activeDetected === p.id && activeType === 'detected'}" title="\${esc(p.name || p.id)}"><strong>\${esc(p.name || p.id)}</strong></button>
|
|
1400
1571
|
<div style="display:flex;gap:6px;align-items:center">
|
|
1401
|
-
<span class="badge"
|
|
1402
|
-
\${canStop ? \`
|
|
1403
|
-
<button class="stop-detected-btn" data-action="stop-detected" data-id="\${esc(p.id)}" title="\${esc(tr('action.stopPeer'))}" aria-label="\${esc(tr('action.stopPeer')) + ' ' + esc(p.id)}" type="button"
|
|
1572
|
+
<span class="badge \${badgeClass(state.label)}" title="\${esc(state.detail)}">\${esc(statusText(state.label))}</span>
|
|
1573
|
+
\${dshCoordinationPeer(p) ? '' : canStop ? \`
|
|
1574
|
+
<button class="session-action stop-detected-btn" data-action="stop-detected" data-id="\${esc(p.id)}" data-focus-key="stop:\${esc(p.id)}" title="\${esc(tr('action.stopPeer'))}" aria-label="\${esc(tr('action.stopPeer')) + ' ' + esc(p.id)}" type="button">✕</button>
|
|
1404
1575
|
\` : \`
|
|
1405
|
-
<button data-action="restart-detected" data-id="\${esc(p.id)}" title="\${esc(tr('action.restartPeer'))}" aria-label="\${esc(tr('action.restartPeer')) + ' ' + esc(p.id)}" type="button"
|
|
1576
|
+
<button class="session-action" data-action="restart-detected" data-id="\${esc(p.id)}" data-focus-key="restart:\${esc(p.id)}" title="\${esc(tr('action.restartPeer'))}" aria-label="\${esc(tr('action.restartPeer')) + ' ' + esc(p.id)}" type="button">↻</button>
|
|
1406
1577
|
\`}
|
|
1407
1578
|
</div>
|
|
1408
1579
|
</div>
|
|
1409
|
-
<div class="
|
|
1410
|
-
|
|
1580
|
+
<div class="session-subtitle"><span class="session-provider">\${esc(p.provider || p.kind || 'other')}</span><span class="path" title="\${esc(p.worktree || p.cwd || '')}">\${esc((p.worktree || p.cwd || '').split('/').filter(Boolean).slice(-2).join('/'))}</span></div>
|
|
1581
|
+
\${sessionDetailsHtml('detected:' + p.id, [[tr('peer'), p.id], [tr('kind'), p.kind], [tr('status'), state.detail], [tr('cwd'), p.worktree || p.cwd], [tr('command'), p.command], [tr('branch'), p.branch], [tr('pid'), p.pid]])}
|
|
1411
1582
|
</div>\`;
|
|
1412
1583
|
};
|
|
1413
1584
|
const activeDetectedHtml = activeDetectedPeers.length
|
|
1414
1585
|
? activeDetectedPeers.map(renderDetectedPeer).join('')
|
|
1415
|
-
: '<div class="empty">' + esc(tr('noActiveDetectedPeers')) +
|
|
1586
|
+
: filtered ? '' : '<div class="empty">' + esc(tr('noActiveDetectedPeers')) + '</div>';
|
|
1416
1587
|
const staleToggleLabel = showStaleDetected ? tr('hideStale') : tr('showStale');
|
|
1417
|
-
const staleDetectedHtml = showStaleDetected
|
|
1588
|
+
const staleDetectedHtml = (showStaleDetected || searching)
|
|
1418
1589
|
? (staleDetectedPeers.length ? staleDetectedPeers.map(renderDetectedPeer).join('') : '<div class="empty">' + esc(tr('noDetectedPeers')) + '</div>')
|
|
1419
1590
|
: '';
|
|
1420
1591
|
|
|
1421
1592
|
const savedScroll = box.scrollTop;
|
|
1422
|
-
|
|
1593
|
+
const restoreFocus = preserveFocus(box);
|
|
1594
|
+
const summaryFocus = document.activeElement?.tagName === 'SUMMARY' && box.contains(document.activeElement) ? document.activeElement.dataset.focusKey : '';
|
|
1595
|
+
const openDetails = new Set([...box.querySelectorAll('details[open]')].map((el) => el.dataset.detailKey));
|
|
1596
|
+
document.getElementById('sessionListCount').textContent = tr('sessionListSummary').replace('{count}', visibleSessions.length + activeDetectedPeers.length + ((showStaleDetected || searching) ? staleDetectedPeers.length : 0));
|
|
1597
|
+
box.innerHTML = filtered && !visibleSessions.length && !visibleDetected.length ? noMatches : \`
|
|
1423
1598
|
<div class="sec-label">\${esc(tr('managed'))} <span class="badge">\${visibleSessions.filter(s=>s.status==='running').length} \${esc(tr('running'))}</span></div>
|
|
1424
1599
|
\${manHtml}
|
|
1425
1600
|
<div class="sec-label" style="margin-top:10px">\${esc(tr('activeDetected'))} <span class="badge" style="color:var(--warn)">\${activeDetectedPeers.length}</span></div>
|
|
1426
1601
|
\${activeDetectedHtml}
|
|
1427
|
-
<div class="sec-label" style="margin-top:10px">\${esc(tr('staleDetected'))} <span class="badge">\${staleDetectedPeers.length}</span><span class="sec-spacer"></span><button id="toggleStaleDetected" type="button">\${esc(staleToggleLabel)}</button></div>
|
|
1602
|
+
<div class="sec-label" style="margin-top:10px">\${esc(tr('staleDetected'))} <span class="badge">\${staleDetectedPeers.length}</span><span class="sec-spacer"></span><button id="toggleStaleDetected" type="button" \${searching ? 'hidden' : ''}>\${esc(staleToggleLabel)}</button></div>
|
|
1428
1603
|
\${staleDetectedHtml}
|
|
1429
1604
|
\`;
|
|
1605
|
+
box.querySelectorAll('details').forEach((el) => { el.open = openDetails.has(el.dataset.detailKey); });
|
|
1430
1606
|
box.scrollTop = savedScroll;
|
|
1607
|
+
restoreFocus();
|
|
1608
|
+
if (summaryFocus) [...box.querySelectorAll('summary')].find((el) => el.dataset.focusKey === summaryFocus)?.focus({ preventScroll: true });
|
|
1609
|
+
box.querySelectorAll('[data-navigation="clear-session-filters"]').forEach((button) => button.addEventListener('click', clearSessionFilters));
|
|
1610
|
+
window.hccCommandPalette?.refresh();
|
|
1611
|
+
box.querySelectorAll('[data-navigation="open-start"]').forEach((button) => button.addEventListener('click', openStartDialog));
|
|
1431
1612
|
const staleToggle = document.getElementById('toggleStaleDetected');
|
|
1432
1613
|
if (staleToggle) {
|
|
1433
1614
|
staleToggle.addEventListener('click', () => {
|
|
1434
1615
|
showStaleDetected = !showStaleDetected;
|
|
1435
|
-
|
|
1616
|
+
hccUi.safeSet('hcc.showStaleDetected', showStaleDetected ? '1' : '0');
|
|
1436
1617
|
renderSections();
|
|
1437
1618
|
});
|
|
1438
1619
|
}
|
|
1439
1620
|
box.querySelectorAll('.session[data-type="managed"]').forEach((el) => {
|
|
1440
|
-
el.addEventListener('click', () =>
|
|
1621
|
+
el.addEventListener('click', (event) => {
|
|
1622
|
+
if (event.target.closest('details, [data-action]')) return;
|
|
1623
|
+
connectManaged(el.dataset.id);
|
|
1624
|
+
});
|
|
1441
1625
|
});
|
|
1442
1626
|
box.querySelectorAll('.session[data-type="detected"]').forEach((el) => {
|
|
1443
1627
|
el.addEventListener('click', (e) => {
|
|
1444
|
-
if (e.target.closest('[data-action]')) return;
|
|
1628
|
+
if (e.target.closest('details, [data-action]')) return;
|
|
1445
1629
|
connectDetected(el.dataset.id);
|
|
1446
1630
|
});
|
|
1447
1631
|
});
|
|
@@ -1451,39 +1635,49 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1451
1635
|
const peerId = btn.dataset.id;
|
|
1452
1636
|
document.getElementById('stopDialogTitle').textContent = tr('stop') + ' ' + peerId + '?';
|
|
1453
1637
|
stopKillCb.checked = false;
|
|
1638
|
+
stopKillCb.parentElement.parentElement.hidden = false;
|
|
1639
|
+
stopConfirmBtn.textContent = tr('stop');
|
|
1454
1640
|
document.getElementById('stopDialogMeta').textContent = tr('detectedPeer');
|
|
1455
1641
|
stopDialog._action = 'detected';
|
|
1456
1642
|
stopDialog._peerId = peerId;
|
|
1457
|
-
stopDialog
|
|
1643
|
+
openDialog(stopDialog, stopCancelBtn);
|
|
1458
1644
|
});
|
|
1459
1645
|
});
|
|
1460
1646
|
box.querySelectorAll('[data-action="restart-detected"]').forEach((btn) => {
|
|
1461
1647
|
btn.addEventListener('click', async (e) => {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1648
|
+
try {
|
|
1649
|
+
e.stopPropagation();
|
|
1650
|
+
const peerId = btn.dataset.id;
|
|
1651
|
+
await api('/api/detected/' + encodeURIComponent(peerId) + '/restart', { method: 'POST', body: '{}' });
|
|
1652
|
+
await Promise.all([refreshSessions(), refreshDetected()]);
|
|
1653
|
+
} catch (error) { handleUiError(error); }
|
|
1466
1654
|
});
|
|
1467
1655
|
});
|
|
1468
1656
|
}
|
|
1469
1657
|
|
|
1470
1658
|
async function refreshSessions() {
|
|
1659
|
+
const root = currentProject;
|
|
1471
1660
|
const data = await api('/api/sessions');
|
|
1661
|
+
if (currentProject !== root) return;
|
|
1472
1662
|
sessions = data.sessions || [];
|
|
1663
|
+
sessionsLoaded = true;
|
|
1473
1664
|
renderSections();
|
|
1474
1665
|
if (active && activeType === 'managed') {
|
|
1475
1666
|
const meta = sessions.find((s) => s.id === active);
|
|
1476
1667
|
if (meta) {
|
|
1477
|
-
|
|
1478
|
-
|
|
1668
|
+
renderActiveSession(meta);
|
|
1669
|
+
activeLocalClients = meta.local_clients ?? activeLocalClients;
|
|
1479
1670
|
}
|
|
1480
1671
|
}
|
|
1481
|
-
connText('online');
|
|
1672
|
+
connText(ws?.readyState === WebSocket.OPEN ? 'attached' : activeDetected ? 'coordinationOnly' : active ? (activeConnectionState === 'connecting' ? 'reconnecting' : 'offline') : 'online');
|
|
1673
|
+
renderHandoff();
|
|
1482
1674
|
}
|
|
1483
1675
|
|
|
1484
1676
|
async function refreshDetected() {
|
|
1677
|
+
const root = currentProject;
|
|
1485
1678
|
try {
|
|
1486
1679
|
const data = await api('/api/detected');
|
|
1680
|
+
if (currentProject !== root) return;
|
|
1487
1681
|
activePeerTtl = Number(data.active_peer_ttl || activePeerTtl);
|
|
1488
1682
|
lastStateNow = Number(data.now || lastStateNow);
|
|
1489
1683
|
detected = data.detected || [];
|
|
@@ -1493,11 +1687,13 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1493
1687
|
|
|
1494
1688
|
// ── Project state panel ───────────────────────────────────────────────
|
|
1495
1689
|
function renderState(data) {
|
|
1690
|
+
lastStateData = data;
|
|
1496
1691
|
const stateRoot = data.root || '';
|
|
1497
1692
|
document.getElementById('rootPath').textContent = stateRoot;
|
|
1498
1693
|
activePeerTtl = Number(data.active_peer_ttl || activePeerTtl);
|
|
1499
1694
|
lastStateNow = Number(data.now || lastStateNow);
|
|
1500
1695
|
const state = document.getElementById('state');
|
|
1696
|
+
const restoreFocus = preserveFocus(state);
|
|
1501
1697
|
const preserveScroll = lastStateRoot === stateRoot;
|
|
1502
1698
|
const savedStateScroll = preserveScroll ? state.scrollTop : 0;
|
|
1503
1699
|
const savedCardScroll = preserveScroll
|
|
@@ -1563,20 +1759,40 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1563
1759
|
else body.scrollTop = saved.top;
|
|
1564
1760
|
}
|
|
1565
1761
|
bindStateCardToggles();
|
|
1762
|
+
window.hccWorkbench.apply();
|
|
1763
|
+
restoreFocus();
|
|
1566
1764
|
lastStateRoot = stateRoot;
|
|
1567
1765
|
}
|
|
1568
1766
|
|
|
1569
1767
|
async function refreshState() {
|
|
1768
|
+
const root = currentProject;
|
|
1769
|
+
const selected = active;
|
|
1770
|
+
const selectionType = activeType;
|
|
1570
1771
|
const peer = active ? managedPeerId(active) : null;
|
|
1571
1772
|
const p = peer ? '/api/state?peer=' + encodeURIComponent(peer) : '/api/state';
|
|
1572
|
-
|
|
1573
|
-
|
|
1773
|
+
try {
|
|
1774
|
+
const data = await api(p);
|
|
1775
|
+
if (currentProject !== root || active !== selected || activeType !== selectionType) return;
|
|
1776
|
+
renderState(data);
|
|
1777
|
+
window.hccWorkbench.fresh();
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
if (currentProject === root && active === selected && activeType === selectionType) window.hccWorkbench.stale(error);
|
|
1780
|
+
throw error;
|
|
1781
|
+
}
|
|
1574
1782
|
}
|
|
1575
1783
|
|
|
1576
1784
|
async function refreshDetectedState() {
|
|
1577
1785
|
if (!activeDetected) return;
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1786
|
+
const root = currentProject, selected = activeDetected;
|
|
1787
|
+
try {
|
|
1788
|
+
const data = await api('/api/state?peer=' + encodeURIComponent(selected));
|
|
1789
|
+
if (currentProject !== root || activeDetected !== selected || activeType !== 'detected') return;
|
|
1790
|
+
renderState(data);
|
|
1791
|
+
window.hccWorkbench.fresh();
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
if (currentProject === root && activeDetected === selected && activeType === 'detected') window.hccWorkbench.stale(error);
|
|
1794
|
+
throw error;
|
|
1795
|
+
}
|
|
1580
1796
|
}
|
|
1581
1797
|
|
|
1582
1798
|
async function refreshCurrentState() {
|
|
@@ -1587,11 +1803,12 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1587
1803
|
async function refreshVisibleData() {
|
|
1588
1804
|
if (autoPollInFlight) return;
|
|
1589
1805
|
autoPollInFlight = true;
|
|
1806
|
+
window.hccWorkbench.loading();
|
|
1590
1807
|
try {
|
|
1591
1808
|
await Promise.all([
|
|
1592
1809
|
refreshSessions(),
|
|
1593
1810
|
refreshDetected(),
|
|
1594
|
-
|
|
1811
|
+
refreshCurrentState()
|
|
1595
1812
|
]);
|
|
1596
1813
|
} finally {
|
|
1597
1814
|
autoPollInFlight = false;
|
|
@@ -1610,19 +1827,32 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1610
1827
|
|
|
1611
1828
|
// ── Connect to managed (PTY) session ─────────────────────────────────
|
|
1612
1829
|
function connectManaged(id) {
|
|
1830
|
+
if (!paneMode) window.hccWorkspace?.primary();
|
|
1613
1831
|
const meta = sessions.find((s) => s.id === id);
|
|
1614
|
-
|
|
1832
|
+
window.hccReview?.reset();
|
|
1833
|
+
disconnectWebSocket();
|
|
1615
1834
|
active = id;
|
|
1616
1835
|
activeDetected = null;
|
|
1617
1836
|
activeType = 'managed';
|
|
1837
|
+
if (paneMode) { const url = new URL(location.href); url.searchParams.set('session', id); history.replaceState(null, '', url); }
|
|
1838
|
+
handoffStore.select(currentProject, 'managed', id);
|
|
1839
|
+
activeLocalClients = meta?.local_clients ?? null;
|
|
1618
1840
|
renderSections();
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1841
|
+
renderActiveSession(meta || { id });
|
|
1842
|
+
const isCodex = meta?.type === 'app-server';
|
|
1843
|
+
const isNative = meta?.type === 'native';
|
|
1844
|
+
document.getElementById('terminal').style.display = isCodex || isNative ? 'none' : '';
|
|
1845
|
+
document.getElementById('codexPanel').hidden = !isCodex;
|
|
1846
|
+
document.getElementById('codexPanel').style.display = isCodex ? '' : 'none';
|
|
1847
|
+
document.getElementById('nativePanel').hidden = !isNative;
|
|
1848
|
+
document.getElementById('terminalComposer').hidden = isCodex || isNative;
|
|
1622
1849
|
document.getElementById('detectedPanel').style.display = 'none';
|
|
1623
1850
|
document.getElementById('quickBar').style.display = '';
|
|
1624
|
-
|
|
1625
|
-
|
|
1851
|
+
setMobileView('terminal');
|
|
1852
|
+
draftReceipt = false;
|
|
1853
|
+
handoffStore.uncertain(currentProject, id);
|
|
1854
|
+
document.getElementById('terminalDraft').value = handoffStore.draft(currentProject, id).text;
|
|
1855
|
+
window.hccTerminalFind?.sync();
|
|
1626
1856
|
term.reset();
|
|
1627
1857
|
terminalHasContent = false;
|
|
1628
1858
|
terminalLastDataAt = 0;
|
|
@@ -1662,13 +1892,20 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1662
1892
|
function openWs(id) {
|
|
1663
1893
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
1664
1894
|
sessionActionTokens.delete(id);
|
|
1895
|
+
sessionControls.delete(id);
|
|
1896
|
+
activeConnectionState = 'connecting';
|
|
1897
|
+
renderHandoff();
|
|
1665
1898
|
const socket = new WebSocket(proto + '://' + location.host + '/ws/terminal/' + encodeURIComponent(id) + requestQuery({ api_version: runtimeApiVersion }));
|
|
1666
1899
|
ws = socket;
|
|
1667
1900
|
socket.onopen = () => {
|
|
1901
|
+
if (ws !== socket || active !== id) return;
|
|
1902
|
+
activeConnectionState = 'connected';
|
|
1903
|
+
renderHandoff();
|
|
1668
1904
|
wsReconnectFailures = 0;
|
|
1669
1905
|
connText('attached');
|
|
1670
1906
|
};
|
|
1671
1907
|
socket.onmessage = (event) => {
|
|
1908
|
+
if (ws !== socket || active !== id) return;
|
|
1672
1909
|
const msg = JSON.parse(event.data);
|
|
1673
1910
|
const pinned = terminalPinned();
|
|
1674
1911
|
// The server streams the tmux pane's raw output, so xterm renders
|
|
@@ -1676,9 +1913,15 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1676
1913
|
// escape sequences carry the cursor.
|
|
1677
1914
|
if (msg.type === 'snapshot') {
|
|
1678
1915
|
if (msg.action_token) sessionActionTokens.set(id, msg.action_token);
|
|
1916
|
+
if (msg.control) sessionControls.set(id, msg.control);
|
|
1917
|
+
if (msg.local_clients != null) activeLocalClients = msg.local_clients;
|
|
1918
|
+
const selected = sessions.find(item => item.id === id);
|
|
1919
|
+
if (selected?.type === 'native' && msg.native_state) window.hccNative?.render(msg.native_state);
|
|
1920
|
+
else if (selected?.type === 'app-server' && msg.state) window.hccCodex?.render(msg.state);
|
|
1921
|
+
renderHandoff();
|
|
1679
1922
|
writeTerminalSnapshot(msg.data || '', true);
|
|
1680
1923
|
resizeTerm();
|
|
1681
|
-
|
|
1924
|
+
// Connection recovery never replays terminal input or a draft.
|
|
1682
1925
|
}
|
|
1683
1926
|
if (msg.type === 'data') {
|
|
1684
1927
|
terminalHasContent = true;
|
|
@@ -1688,15 +1931,41 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1688
1931
|
if (msg.type === 'replace' && shouldApplyTerminalReplace()) {
|
|
1689
1932
|
writeTerminalSnapshot(msg.data || '', pinned);
|
|
1690
1933
|
}
|
|
1934
|
+
if (msg.type === 'control') {
|
|
1935
|
+
sessionControls.set(id, msg.control || {});
|
|
1936
|
+
lastSentTerminalSize = null;
|
|
1937
|
+
renderHandoff();
|
|
1938
|
+
resizeTerm();
|
|
1939
|
+
window.hccCodex?.sync();
|
|
1940
|
+
}
|
|
1941
|
+
if (msg.type === 'codex_state') window.hccCodex?.render(msg.state);
|
|
1942
|
+
if (msg.type === 'native_state') window.hccNative?.render(msg.state);
|
|
1943
|
+
if (msg.type === 'input_ack' && handoffStore.acknowledge(currentProject, id, msg.input_id)) {
|
|
1944
|
+
draftReceipt = true;
|
|
1945
|
+
document.getElementById('terminalDraft').value = handoffStore.draft(currentProject, id).text;
|
|
1946
|
+
renderDraftState();
|
|
1947
|
+
}
|
|
1948
|
+
if (msg.type === 'error') {
|
|
1949
|
+
if (msg.control) sessionControls.set(id, msg.control);
|
|
1950
|
+
renderHandoff();
|
|
1951
|
+
handoffStore.uncertain(currentProject, id);
|
|
1952
|
+
renderDraftState();
|
|
1953
|
+
handleUiError({ code: msg.error?.code, message: msg.error?.message || tr('requestFailed') });
|
|
1954
|
+
}
|
|
1691
1955
|
if (msg.type === 'exit') { refreshSessions().catch(console.error); }
|
|
1692
1956
|
};
|
|
1693
1957
|
socket.onclose = (event) => {
|
|
1694
|
-
sessionActionTokens.delete(id);
|
|
1695
1958
|
if (ws !== socket) return;
|
|
1959
|
+
sessionActionTokens.delete(id);
|
|
1960
|
+
sessionControls.delete(id);
|
|
1961
|
+
activeConnectionState = 'disconnected';
|
|
1962
|
+
handoffStore.uncertain(currentProject, id);
|
|
1963
|
+
renderHandoff();
|
|
1964
|
+
window.hccCodex?.sync();
|
|
1696
1965
|
// ui-4: no reconnect after logout, and stop after repeated failures
|
|
1697
1966
|
// (e.g. the session cookie was revoked in another tab) instead of
|
|
1698
1967
|
// looping every 2s forever.
|
|
1699
|
-
if (event.code === 4001 ||
|
|
1968
|
+
if (event.code === 4001 || loggedOut) {
|
|
1700
1969
|
connText('signed out');
|
|
1701
1970
|
return;
|
|
1702
1971
|
}
|
|
@@ -1717,17 +1986,27 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1717
1986
|
|
|
1718
1987
|
// ── Connect to detected (coordination-only) peer ──────────────────────
|
|
1719
1988
|
function connectDetected(id) {
|
|
1989
|
+
if (!paneMode) window.hccWorkspace?.primary();
|
|
1720
1990
|
const peer = detected.find((p) => p.id === id);
|
|
1991
|
+
window.hccReview?.reset();
|
|
1992
|
+
disconnectWebSocket();
|
|
1721
1993
|
active = null;
|
|
1722
1994
|
activeDetected = id;
|
|
1723
1995
|
activeType = 'detected';
|
|
1724
|
-
|
|
1996
|
+
handoffStore.select(currentProject, 'detected', id);
|
|
1997
|
+
renderHandoff();
|
|
1725
1998
|
renderSections();
|
|
1726
1999
|
document.getElementById('activeTitle').textContent = id + ' (' + tr('detected') + ')';
|
|
1727
2000
|
document.getElementById('activeMeta').textContent = peer ? peer.kind + ' · ' + (peer.worktree || '') : '';
|
|
1728
2001
|
document.getElementById('terminal').style.display = 'none';
|
|
2002
|
+
document.getElementById('codexPanel').hidden = true;
|
|
2003
|
+
document.getElementById('nativePanel').hidden = true;
|
|
2004
|
+
document.getElementById('codexPanel').style.display = 'none';
|
|
2005
|
+
document.getElementById('terminalComposer').hidden = true;
|
|
2006
|
+
document.getElementById('activeTask').hidden = true;
|
|
1729
2007
|
document.getElementById('detectedPanel').style.display = '';
|
|
1730
2008
|
document.getElementById('quickBar').style.display = 'none';
|
|
2009
|
+
setMobileView('terminal');
|
|
1731
2010
|
connText('coordinationOnly');
|
|
1732
2011
|
renderDetectedPanel(peer || { id });
|
|
1733
2012
|
refreshDetectedState().catch(console.error);
|
|
@@ -1742,7 +2021,11 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1742
2021
|
<div class="body">
|
|
1743
2022
|
<div class="item"><strong>\${esc(tr('peer'))}</strong><span class="mono">\${esc(peer.id)}</span></div>
|
|
1744
2023
|
<div class="item"><strong>\${esc(tr('kind'))}</strong><span>\${esc(peer.kind || '')}</span></div>
|
|
1745
|
-
|
|
2024
|
+
\${dshCoordinationPeer(peer) ? \`
|
|
2025
|
+
<div class="item"><strong>\${esc(tr('providerSession'))}</strong><span class="mono" style="overflow-wrap:anywhere">\${esc(peer.provider_session_id || tr('unknown'))}</span></div>
|
|
2026
|
+
<p style="font-size:12px;color:var(--muted)">\${esc(tr('dshCoordinationHelp'))}</p>
|
|
2027
|
+
\` : ''}
|
|
2028
|
+
<div class="item"><strong>\${esc(tr('status'))}</strong><span>\${esc(statusText(peer.status))}</span></div>
|
|
1746
2029
|
<div class="item"><strong>\${esc(tr('cwd'))}</strong><span class="mono" style="font-size:11px">\${esc(peer.worktree || '')}</span></div>
|
|
1747
2030
|
<div class="item"><strong>\${esc(tr('pid'))}</strong><span>\${esc(peer.pid || tr('unknown'))}</span></div>
|
|
1748
2031
|
<div class="item"><strong>\${esc(tr('lastSeen'))}</strong><span>\${peer.age_sec != null ? esc(peer.age_sec + tr('secondsAgo')) : ''}</span></div>
|
|
@@ -1751,24 +2034,34 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1751
2034
|
<div class="card">
|
|
1752
2035
|
<h2>\${esc(tr('sendMessage'))}</h2>
|
|
1753
2036
|
<div class="body" style="gap:8px">
|
|
1754
|
-
<div style="font-size:12px;color:var(--muted)">\${tr('messageHelp')}</div>
|
|
1755
|
-
<textarea id="detMsg" rows="3" style="width:100%;background
|
|
2037
|
+
<div style="font-size:12px;color:var(--muted)">\${tr(dshCoordinationPeer(peer) && peer.transport === 'cordis' ? 'dshCordisMessageHelp' : 'messageHelp')}</div>
|
|
2038
|
+
<textarea id="detMsg" rows="3" style="width:100%;background:var(--input-bg);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:8px;font:inherit;resize:vertical" placeholder="\${esc(tr('messageBodyPlaceholder'))}"></textarea>
|
|
1756
2039
|
<button class="primary" id="sendDetMsg">\${esc(tr('send'))}</button>
|
|
1757
2040
|
</div>
|
|
1758
2041
|
</div>
|
|
1759
2042
|
</div>
|
|
1760
2043
|
\`;
|
|
2044
|
+
const draftProject = currentProject;
|
|
2045
|
+
const draftKey = 'detected:' + peer.id;
|
|
2046
|
+
document.getElementById('detMsg').value = handoffStore.draft(draftProject, draftKey).text;
|
|
2047
|
+
document.getElementById('detMsg').addEventListener('input', (event) => handoffStore.saveDraft(draftProject, draftKey, event.target.value));
|
|
1761
2048
|
document.getElementById('sendDetMsg').addEventListener('click', async () => {
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
2049
|
+
try {
|
|
2050
|
+
const body = document.getElementById('detMsg').value.trim();
|
|
2051
|
+
if (!body) return;
|
|
2052
|
+
await api('/api/detected/' + encodeURIComponent(peer.id) + '/msg', {
|
|
2053
|
+
method: 'POST',
|
|
2054
|
+
body: JSON.stringify({ body })
|
|
1767
2055
|
});
|
|
1768
|
-
document.getElementById('detMsg')
|
|
2056
|
+
const textarea = document.getElementById('detMsg');
|
|
2057
|
+
if (currentProject === draftProject && activeDetected === peer.id && textarea?.value.trim() === body) {
|
|
2058
|
+
textarea.value = '';
|
|
2059
|
+
handoffStore.saveDraft(draftProject, draftKey, '');
|
|
2060
|
+
}
|
|
1769
2061
|
await refreshDetectedState();
|
|
1770
|
-
});
|
|
1771
|
-
}
|
|
2062
|
+
} catch (error) { handleUiError(error); }
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
1772
2065
|
|
|
1773
2066
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
1774
2067
|
function sendLine(text) {
|
|
@@ -1777,24 +2070,52 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1777
2070
|
|
|
1778
2071
|
// ── Start session form ────────────────────────────────────────────────
|
|
1779
2072
|
document.getElementById('kind').addEventListener('change', syncStartModeOptions);
|
|
2073
|
+
document.getElementById('transport').addEventListener('change', syncStartModeOptions);
|
|
1780
2074
|
document.getElementById('startMode').addEventListener('change', syncStartModeOptions);
|
|
1781
2075
|
document.getElementById('resumeSelect').addEventListener('change', toggleResumeCustom);
|
|
1782
2076
|
syncStartModeOptions();
|
|
1783
2077
|
|
|
1784
2078
|
document.getElementById('startForm').addEventListener('submit', async (event) => {
|
|
1785
2079
|
event.preventDefault();
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2080
|
+
if (startPending) return;
|
|
2081
|
+
startPending = true;
|
|
2082
|
+
document.getElementById('openStartDialog').disabled = true;
|
|
2083
|
+
const submitButton = event.currentTarget.querySelector('button[type="submit"]');
|
|
2084
|
+
submitButton.disabled = true;
|
|
2085
|
+
const errorElement = document.getElementById('startDialogError');
|
|
2086
|
+
const projectRoot = currentProject;
|
|
2087
|
+
errorElement.hidden = true;
|
|
2088
|
+
try {
|
|
2089
|
+
const kind = document.getElementById('kind').value;
|
|
2090
|
+
const mode = document.getElementById('startMode').value;
|
|
2091
|
+
const sel = document.getElementById('resumeSelect');
|
|
2092
|
+
const resume = (sel.value && sel.value !== '__custom__')
|
|
2093
|
+
? sel.value
|
|
2094
|
+
: document.getElementById('resumeArg').value.trim();
|
|
2095
|
+
if (mode === 'resume' && !resume) {
|
|
2096
|
+
document.getElementById('resumeArg').focus();
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const payload = { kind, mode };
|
|
2100
|
+
if (kind === 'codex' && document.getElementById('transport').value === 'app-server') {
|
|
2101
|
+
payload.transport = 'app-server';
|
|
2102
|
+
if (mode === 'resume') {
|
|
2103
|
+
if (!document.getElementById('appServerResumeConfirm').checked) throw new Error(tr('handoff.resumeConfirm'));
|
|
2104
|
+
payload.handoffConfirmed = true;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (mode === 'resume') payload.resume = resume;
|
|
2108
|
+
const data = await api('/api/sessions', { method: 'POST', body: JSON.stringify(payload) });
|
|
2109
|
+
if (currentProject !== projectRoot) return;
|
|
2110
|
+
await refreshSessions();
|
|
2111
|
+
if (currentProject !== projectRoot || startDialog.hidden) return;
|
|
2112
|
+
closeDialog(startDialog);
|
|
2113
|
+
connectManaged(data.session.id);
|
|
2114
|
+
} catch (error) {
|
|
2115
|
+
errorElement.textContent = tr('error.' + error.code, error.detail || error.message);
|
|
2116
|
+
errorElement.hidden = false;
|
|
2117
|
+
}
|
|
2118
|
+
finally { startPending = false; submitButton.disabled = false; document.getElementById('openStartDialog').disabled = false; }
|
|
1798
2119
|
});
|
|
1799
2120
|
|
|
1800
2121
|
function activePeerInfo() {
|
|
@@ -1826,52 +2147,80 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1826
2147
|
function formatActionResult(result) {
|
|
1827
2148
|
if (!result) return '';
|
|
1828
2149
|
const data = result.data || {};
|
|
2150
|
+
const error = result.error || data.error;
|
|
2151
|
+
if (error) {
|
|
2152
|
+
const code = String(result.errorCode || '').toLowerCase();
|
|
2153
|
+
return [
|
|
2154
|
+
tr('error.' + code, tr('requestFailed')),
|
|
2155
|
+
result.errorCode ? '[' + result.errorCode + ']' : '',
|
|
2156
|
+
tr('details') + ': ' + error
|
|
2157
|
+
].join('\\n');
|
|
2158
|
+
}
|
|
1829
2159
|
if (result.action === 'status') {
|
|
1830
|
-
const tasks = (data.tasks || []).map((row) => row.status + ':' + row.n).join(', ') || 'none';
|
|
2160
|
+
const tasks = (data.tasks || []).map((row) => statusText(row.status) + ': ' + row.n).join(', ') || tr('none');
|
|
1831
2161
|
return [
|
|
1832
|
-
data.root,
|
|
1833
|
-
'peers active
|
|
1834
|
-
'tasks ' + tasks,
|
|
1835
|
-
'locks active
|
|
1836
|
-
'unread ' + (data.unread ?? 0)
|
|
2162
|
+
data.root ? tr('root') + ': ' + data.root : '',
|
|
2163
|
+
tr('peers') + ' · ' + tr('active') + ': ' + (data.active_peers ?? 0) + ' · ' + tr('stale') + ': ' + (data.stale_peers ?? 0),
|
|
2164
|
+
tr('tasks') + ': ' + tasks,
|
|
2165
|
+
tr('locks') + ' · ' + tr('active') + ': ' + (data.active_locks ?? 0),
|
|
2166
|
+
tr('unread') + ': ' + (data.unread ?? 0)
|
|
1837
2167
|
].filter(Boolean).join('\\n');
|
|
1838
2168
|
}
|
|
1839
2169
|
if (result.action === 'state') {
|
|
1840
2170
|
const automation = data.automation || {};
|
|
1841
2171
|
const next = automation.next_action || {};
|
|
1842
2172
|
return [
|
|
1843
|
-
'phase: ' + (automation.phase || 'idle'),
|
|
1844
|
-
'next: ' + (next.command || next.kind
|
|
1845
|
-
'why: ' + (next.reason || tr('noImmediateAction')),
|
|
1846
|
-
...(automation.warnings || []).map((w) => '
|
|
2173
|
+
tr('phase') + ': ' + statusText(automation.phase || 'idle'),
|
|
2174
|
+
tr('next') + ': ' + (next.command || (next.kind && next.kind !== 'none' ? tr('action.' + next.kind, next.kind) : tr('none'))),
|
|
2175
|
+
tr('why') + ': ' + (next.reason || tr('noImmediateAction')),
|
|
2176
|
+
...(automation.warnings || []).map((w) => tr('warnings') + ': ' + w)
|
|
1847
2177
|
].join('\\n');
|
|
1848
2178
|
}
|
|
1849
2179
|
if (result.action === 'inbox') {
|
|
1850
2180
|
const messages = data.messages || [];
|
|
1851
2181
|
return messages.length
|
|
1852
|
-
? messages.map((m) => '#' + m.id + ' ' + m.sender + ' -> ' + (m.recipient || 'all') + ': ' + m.body).join('\\n')
|
|
2182
|
+
? messages.map((m) => '#' + m.id + ' ' + m.sender + ' -> ' + (m.recipient || tr('all')) + ': ' + m.body).join('\\n')
|
|
1853
2183
|
: tr('noMessages');
|
|
1854
2184
|
}
|
|
1855
2185
|
if (result.action === 'task-next') {
|
|
1856
2186
|
return data.task
|
|
1857
|
-
? (data.current ? 'current
|
|
1858
|
-
:
|
|
2187
|
+
? tr(data.current ? 'current' : 'claimed') + ': #' + data.task.id + ' ' + data.task.title + ' (' + statusText(data.task.status) + ')'
|
|
2188
|
+
: tr('noPendingTasks');
|
|
1859
2189
|
}
|
|
1860
2190
|
if (result.action === 'task-takeover') {
|
|
1861
|
-
return data.task ? '#' + data.task.id + ' ' + data.task.title + '
|
|
2191
|
+
return data.task ? '#' + data.task.id + ' ' + data.task.title + ' · ' + tr('owner') + ': ' + data.task.owner : tr('noTasks');
|
|
1862
2192
|
}
|
|
1863
2193
|
if (result.action === 'lock-acquire') {
|
|
1864
|
-
return data.lock ? 'locked ' + lockLabel(data.lock) + ' by ' + data.lock.owner :
|
|
2194
|
+
return data.lock ? tr('locked') + ': ' + lockLabel(data.lock) + ' · ' + tr('by') + ': ' + data.lock.owner : tr('noActiveLocks');
|
|
1865
2195
|
}
|
|
1866
2196
|
if (result.action === 'lock-release') {
|
|
1867
|
-
return result
|
|
2197
|
+
return data.result ? tr(data.result.released ? 'lockReleased' : 'noLock') + ': ' + lockLabel(data.result) : tr('noActiveLocks');
|
|
2198
|
+
}
|
|
2199
|
+
if (result.action === 'heartbeat') {
|
|
2200
|
+
return [
|
|
2201
|
+
tr('peer') + ': ' + (data.peer || result.peer || tr('unknown')),
|
|
2202
|
+
data.status ? tr('status') + ': ' + statusText(data.status) : '',
|
|
2203
|
+
tr('renewedLocks') + ': ' + (data.renewed ?? 0)
|
|
2204
|
+
].filter(Boolean).join('\\n');
|
|
2205
|
+
}
|
|
2206
|
+
if (result.action === 'register') {
|
|
2207
|
+
const peer = data.peer || {};
|
|
2208
|
+
return [
|
|
2209
|
+
tr('registered') + ': ' + (peer.id || result.peer || tr('unknown')),
|
|
2210
|
+
tr('kind') + ': ' + (peer.kind || tr('unknown')),
|
|
2211
|
+
tr('status') + ': ' + statusText(peer.status),
|
|
2212
|
+
peer.worktree ? tr('cwd') + ': ' + peer.worktree : ''
|
|
2213
|
+
].filter(Boolean).join('\\n');
|
|
1868
2214
|
}
|
|
1869
2215
|
return result.summary || JSON.stringify(result, null, 2);
|
|
1870
2216
|
}
|
|
1871
2217
|
|
|
1872
2218
|
function showActionResult(result) {
|
|
2219
|
+
lastActionResult = result;
|
|
1873
2220
|
const panel = document.getElementById('actionResult');
|
|
1874
|
-
|
|
2221
|
+
const action = String(result.action || '');
|
|
2222
|
+
const label = action ? tr('action.' + action, tr(action, action)) : tr('actionResult');
|
|
2223
|
+
document.getElementById('actionResultTitle').textContent = label + (result.peer ? ' · ' + result.peer : '');
|
|
1875
2224
|
document.getElementById('actionResultBody').textContent = formatActionResult(result);
|
|
1876
2225
|
panel.hidden = false;
|
|
1877
2226
|
}
|
|
@@ -1885,7 +2234,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1885
2234
|
: action === 'heartbeat'
|
|
1886
2235
|
? { renew_locks: true }
|
|
1887
2236
|
: {};
|
|
1888
|
-
if (!readOnly) payload.action_token = info.actionToken;
|
|
2237
|
+
if (!readOnly) { payload.action_token = info.actionToken; payload.epoch = controlEpoch(); }
|
|
1889
2238
|
const result = await api('/api/peers/' + encodeURIComponent(info.peerId) + '/actions/' + encodeURIComponent(action), readOnly
|
|
1890
2239
|
? {}
|
|
1891
2240
|
: { method: 'POST', body: JSON.stringify(payload) });
|
|
@@ -1897,7 +2246,7 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1897
2246
|
button.addEventListener('click', () => {
|
|
1898
2247
|
closeActionsMenu();
|
|
1899
2248
|
runPeerAction(button.dataset.action).catch((err) => {
|
|
1900
|
-
|
|
2249
|
+
handleUiError(err, button.dataset.action);
|
|
1901
2250
|
});
|
|
1902
2251
|
});
|
|
1903
2252
|
});
|
|
@@ -1911,12 +2260,15 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1911
2260
|
});
|
|
1912
2261
|
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
|
1913
2262
|
closeActionsMenu();
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2263
|
+
try {
|
|
2264
|
+
const res = await fetch('/logout', { method: 'POST', headers });
|
|
2265
|
+
if (!res.ok) throw new Error(tr('requestFailed'));
|
|
2266
|
+
// Stop reconnect only after the server confirms revocation.
|
|
2267
|
+
loggedOut = true;
|
|
2268
|
+
window.hccWorkspace?.destroy();
|
|
2269
|
+
try { sessionStorage.setItem('hcc_logged_out', '1'); } catch {}
|
|
2270
|
+
location.replace('/');
|
|
2271
|
+
} catch (error) { handleUiError(error, 'logout'); }
|
|
1920
2272
|
});
|
|
1921
2273
|
document.getElementById('actionResultClose').addEventListener('click', () => {
|
|
1922
2274
|
document.getElementById('actionResult').hidden = true;
|
|
@@ -1927,6 +2279,87 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1927
2279
|
const stopConfirmBtn = document.getElementById('stopConfirmBtn');
|
|
1928
2280
|
const stopCancelBtn = document.getElementById('stopCancelBtn');
|
|
1929
2281
|
const stopKillCb = document.getElementById('stopKillCb');
|
|
2282
|
+
const settingsDialog = document.getElementById('settingsDialog');
|
|
2283
|
+
const projectDialog = document.getElementById('projectDialog');
|
|
2284
|
+
const startDialog = document.getElementById('startDialog');
|
|
2285
|
+
let dialogReturnFocus = null;
|
|
2286
|
+
function openDialog(dialog, initialFocus) {
|
|
2287
|
+
closeActionsMenu();
|
|
2288
|
+
dialogReturnFocus = document.activeElement;
|
|
2289
|
+
dialog.hidden = false;
|
|
2290
|
+
document.querySelector('.app').inert = true;
|
|
2291
|
+
document.querySelector('.global-header').inert = true;
|
|
2292
|
+
document.querySelector('.mobile-nav').inert = true;
|
|
2293
|
+
initialFocus?.focus();
|
|
2294
|
+
}
|
|
2295
|
+
function closeDialog(dialog) {
|
|
2296
|
+
dialog.hidden = true;
|
|
2297
|
+
document.querySelector('.app').inert = false;
|
|
2298
|
+
document.querySelector('.global-header').inert = false;
|
|
2299
|
+
document.querySelector('.mobile-nav').inert = false;
|
|
2300
|
+
if (dialogReturnFocus?.isConnected && !dialogReturnFocus.disabled && dialogReturnFocus.getClientRects().length) dialogReturnFocus.focus();
|
|
2301
|
+
else document.getElementById('settingsBtn').focus();
|
|
2302
|
+
dialogReturnFocus = null;
|
|
2303
|
+
}
|
|
2304
|
+
function updateStopDialogTitle() {
|
|
2305
|
+
const peerId = stopDialog._action === 'detected' ? stopDialog._peerId : (sessionPeerId(sessions.find((s) => s.id === active)) || active);
|
|
2306
|
+
document.getElementById('stopDialogTitle').textContent = tr('stop') + ' ' + (peerId || '') + '?';
|
|
2307
|
+
if (stopDialog._action === 'detected') {
|
|
2308
|
+
document.getElementById('stopDialogMeta').textContent = tr('detectedPeer');
|
|
2309
|
+
stopConfirmBtn.textContent = tr('stop');
|
|
2310
|
+
} else {
|
|
2311
|
+
const session = sessions.find((item) => item.id === active) || {};
|
|
2312
|
+
document.getElementById('stopDialogTitle').textContent = tr(session.type === 'tmux' ? 'handoff.detach' : 'handoff.terminate') + ' ' + (peerId || '') + '?';
|
|
2313
|
+
stopConfirmBtn.textContent = tr(session.type === 'tmux' ? 'handoff.detach' : 'handoff.terminate');
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function openStartDialog() {
|
|
2317
|
+
if (startPending) return;
|
|
2318
|
+
document.getElementById('startProjectPath').textContent = currentProject;
|
|
2319
|
+
document.getElementById('startDialogError').hidden = true;
|
|
2320
|
+
document.getElementById('appServerResumeConfirm').checked = false;
|
|
2321
|
+
syncStartModeOptions();
|
|
2322
|
+
openDialog(startDialog, document.getElementById('kind'));
|
|
2323
|
+
}
|
|
2324
|
+
document.getElementById('openStartDialog').addEventListener('click', openStartDialog);
|
|
2325
|
+
document.getElementById('openHistoryDialog').addEventListener('click', () => window.hccHistory.open());
|
|
2326
|
+
document.getElementById('openReviewDialog').addEventListener('click', () => window.hccReview.open());
|
|
2327
|
+
document.getElementById('startDialogClose').addEventListener('click', () => closeDialog(startDialog));
|
|
2328
|
+
document.getElementById('startCancelBtn').addEventListener('click', () => closeDialog(startDialog));
|
|
2329
|
+
document.getElementById('openProjectDialog').addEventListener('click', () => {
|
|
2330
|
+
document.getElementById('projectDialogError').hidden = true;
|
|
2331
|
+
openDialog(projectDialog, document.getElementById('projectPath'));
|
|
2332
|
+
});
|
|
2333
|
+
document.getElementById('projectDialogClose').addEventListener('click', () => closeDialog(projectDialog));
|
|
2334
|
+
document.getElementById('projectCancelBtn').addEventListener('click', () => closeDialog(projectDialog));
|
|
2335
|
+
document.getElementById('settingsBtn').addEventListener('click', () => openDialog(settingsDialog, document.getElementById('langSelect')));
|
|
2336
|
+
document.getElementById('settingsClose').addEventListener('click', () => closeDialog(settingsDialog));
|
|
2337
|
+
[settingsDialog, stopDialog, projectDialog, startDialog, document.getElementById('historyDialog'), document.getElementById('reviewDialog'), document.getElementById('commandDialog')].forEach((dialog) => {
|
|
2338
|
+
dialog.addEventListener('click', (event) => { if (event.target === dialog) closeDialog(dialog); });
|
|
2339
|
+
dialog.addEventListener('keydown', (event) => {
|
|
2340
|
+
if (event.isComposing) return;
|
|
2341
|
+
if (event.key === 'Escape') { event.preventDefault(); closeDialog(dialog); return; }
|
|
2342
|
+
if (event.key !== 'Tab') return;
|
|
2343
|
+
const controls = [...dialog.querySelectorAll('button, select, input, textarea, [tabindex="0"]')].filter((el) => !el.disabled && el.tabIndex >= 0 && el.getClientRects().length);
|
|
2344
|
+
const first = controls[0], last = controls[controls.length - 1];
|
|
2345
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
|
2346
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
function preserveFocus(root) {
|
|
2350
|
+
const focused = document.activeElement;
|
|
2351
|
+
if (!root.contains(focused)) return () => {};
|
|
2352
|
+
const key = focused.dataset.focusKey;
|
|
2353
|
+
const id = focused.id;
|
|
2354
|
+
const section = focused.closest('[data-section]')?.dataset.section;
|
|
2355
|
+
const action = focused.dataset.action;
|
|
2356
|
+
const peerId = focused.dataset.id;
|
|
2357
|
+
return () => {
|
|
2358
|
+
const target = [...root.querySelectorAll('button, select, input')].find((el) =>
|
|
2359
|
+
key ? el.dataset.focusKey === key : id ? el.id === id : section ? el.closest('[data-section]')?.dataset.section === section : action && el.dataset.action === action && el.dataset.id === peerId);
|
|
2360
|
+
target?.focus({ preventScroll: true });
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
1930
2363
|
|
|
1931
2364
|
// ── Actions dropdown (declutters the toolbar) ─────────────────────────
|
|
1932
2365
|
const actionsBtn = document.getElementById('actionsBtn');
|
|
@@ -1954,6 +2387,41 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1954
2387
|
|
|
1955
2388
|
// ── Collapsible side panels ───────────────────────────────────────────
|
|
1956
2389
|
const appEl = document.querySelector('.app');
|
|
2390
|
+
const narrowViewport = window.matchMedia('(max-width: 1099px)');
|
|
2391
|
+
const focusBtn = document.getElementById('focusBtn');
|
|
2392
|
+
function syncFocusButton() {
|
|
2393
|
+
const enabled = appEl.classList.contains('focus-mode');
|
|
2394
|
+
focusBtn.textContent = tr(enabled ? 'exitFocus' : 'focusMode');
|
|
2395
|
+
focusBtn.setAttribute('aria-pressed', String(enabled));
|
|
2396
|
+
}
|
|
2397
|
+
function syncPanelAccess() {
|
|
2398
|
+
const focus = appEl.classList.contains('focus-mode') && !narrowViewport.matches;
|
|
2399
|
+
for (const [selector, view, side] of [['.sidebar', 'sessions', 'left'], ['.main', 'terminal', null], ['.inspector', 'state', 'right']]) {
|
|
2400
|
+
appEl.querySelector(selector).inert = paneMode ? view !== 'terminal' : narrowViewport.matches
|
|
2401
|
+
? appEl.dataset.view !== view || view === 'terminal' && appEl.classList.contains('workspace-open') && document.getElementById('workspace').dataset.activePane === 'secondary'
|
|
2402
|
+
: Boolean(side && (focus || sideIsCollapsed(side) || side === 'right' && appEl.classList.contains('workspace-open')));
|
|
2403
|
+
}
|
|
2404
|
+
document.getElementById('workspaceSecondPane').inert = narrowViewport.matches && (appEl.dataset.view !== 'terminal' || document.getElementById('workspace').dataset.activePane !== 'secondary');
|
|
2405
|
+
}
|
|
2406
|
+
function setMobileView(view) {
|
|
2407
|
+
if (paneMode) view = 'terminal';
|
|
2408
|
+
if (!['sessions', 'terminal', 'state'].includes(view)) return;
|
|
2409
|
+
appEl.dataset.view = view;
|
|
2410
|
+
document.querySelectorAll('.mobile-nav [data-view]').forEach((button) => {
|
|
2411
|
+
if (button.dataset.view === view) button.setAttribute('aria-current', 'page');
|
|
2412
|
+
else button.removeAttribute('aria-current');
|
|
2413
|
+
});
|
|
2414
|
+
syncPanelAccess();
|
|
2415
|
+
closeActionsMenu();
|
|
2416
|
+
requestAnimationFrame(() => resizeTerm());
|
|
2417
|
+
}
|
|
2418
|
+
document.querySelectorAll('.mobile-nav [data-view]').forEach((button) => button.addEventListener('click', () => setMobileView(button.dataset.view)));
|
|
2419
|
+
focusBtn.addEventListener('click', () => {
|
|
2420
|
+
appEl.classList.toggle('focus-mode');
|
|
2421
|
+
syncFocusButton();
|
|
2422
|
+
syncPanelAccess();
|
|
2423
|
+
setTimeout(resizeTerm, 200);
|
|
2424
|
+
});
|
|
1957
2425
|
const toggleLeftBtn = document.getElementById('toggleLeft');
|
|
1958
2426
|
const toggleRightBtn = document.getElementById('toggleRight');
|
|
1959
2427
|
const resizeLeftHandle = document.getElementById('resizeLeft');
|
|
@@ -1964,10 +2432,10 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1964
2432
|
const sideWidthKey = { left: 'hcc.sidebar.left.width', right: 'hcc.sidebar.right.width' };
|
|
1965
2433
|
const dragState = { side: null, startX: 0, startWidth: 0, moved: false, suppressClick: false };
|
|
1966
2434
|
function sideIsCollapsed(side) {
|
|
1967
|
-
return appEl.classList.contains(side + '-collapsed') ||
|
|
2435
|
+
return appEl.classList.contains(side + '-collapsed') || hccUi.safeGet('hcc.collapse.' + side) === '1';
|
|
1968
2436
|
}
|
|
1969
2437
|
function storedSideWidth(side) {
|
|
1970
|
-
const raw = Number(
|
|
2438
|
+
const raw = Number(hccUi.safeGet(sideWidthKey[side]));
|
|
1971
2439
|
return Number.isFinite(raw) && raw > 0 ? raw : sideDefaults[side];
|
|
1972
2440
|
}
|
|
1973
2441
|
function effectiveOppositeWidth(side) {
|
|
@@ -1986,7 +2454,11 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
1986
2454
|
function setSideWidth(side, width, persist = true) {
|
|
1987
2455
|
const value = clampSideWidth(side, width);
|
|
1988
2456
|
appEl.style.setProperty('--' + side + '-width', value + 'px');
|
|
1989
|
-
if (persist)
|
|
2457
|
+
if (persist) hccUi.safeSet(sideWidthKey[side], String(value));
|
|
2458
|
+
const handle = document.getElementById(side === 'left' ? 'resizeLeft' : 'resizeRight');
|
|
2459
|
+
handle.setAttribute('aria-valuemin', String(sideMin[side]));
|
|
2460
|
+
handle.setAttribute('aria-valuemax', String(clampSideWidth(side, sideMax[side])));
|
|
2461
|
+
handle.setAttribute('aria-valuenow', String(value));
|
|
1990
2462
|
return value;
|
|
1991
2463
|
}
|
|
1992
2464
|
function applySideWidths() {
|
|
@@ -2002,17 +2474,19 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
2002
2474
|
toggleRightBtn.title = (r ? tr('show') : tr('collapse')) + ' ' + tr('statePanel') + '; ' + tr('dragToResize');
|
|
2003
2475
|
}
|
|
2004
2476
|
function applyCollapseState() {
|
|
2005
|
-
appEl.classList.toggle('left-collapsed',
|
|
2006
|
-
appEl.classList.toggle('right-collapsed',
|
|
2477
|
+
appEl.classList.toggle('left-collapsed', hccUi.safeGet('hcc.collapse.left') === '1');
|
|
2478
|
+
appEl.classList.toggle('right-collapsed', hccUi.safeGet('hcc.collapse.right') === '1');
|
|
2007
2479
|
applySideWidths();
|
|
2008
2480
|
syncToggleIcons();
|
|
2481
|
+
syncPanelAccess();
|
|
2009
2482
|
}
|
|
2010
2483
|
function toggleSide(side) {
|
|
2011
2484
|
const cls = side + '-collapsed';
|
|
2012
2485
|
const on = appEl.classList.toggle(cls);
|
|
2013
|
-
|
|
2486
|
+
hccUi.safeSet('hcc.collapse.' + side, on ? '1' : '0');
|
|
2014
2487
|
applySideWidths();
|
|
2015
2488
|
syncToggleIcons();
|
|
2489
|
+
syncPanelAccess();
|
|
2016
2490
|
// Refit the terminal after the grid transition so xterm uses the new width.
|
|
2017
2491
|
setTimeout(() => { try { resizeTerm(); } catch {} }, 200);
|
|
2018
2492
|
}
|
|
@@ -2038,12 +2512,13 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
2038
2512
|
setSideWidth(dragState.side, width);
|
|
2039
2513
|
if (dragState.side === 'left') {
|
|
2040
2514
|
appEl.classList.remove('left-collapsed');
|
|
2041
|
-
|
|
2515
|
+
hccUi.safeSet('hcc.collapse.left', '0');
|
|
2042
2516
|
} else {
|
|
2043
2517
|
appEl.classList.remove('right-collapsed');
|
|
2044
|
-
|
|
2518
|
+
hccUi.safeSet('hcc.collapse.right', '0');
|
|
2045
2519
|
}
|
|
2046
2520
|
syncToggleIcons();
|
|
2521
|
+
syncPanelAccess();
|
|
2047
2522
|
try { resizeTerm(); } catch {}
|
|
2048
2523
|
event.preventDefault();
|
|
2049
2524
|
}
|
|
@@ -2057,6 +2532,19 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
2057
2532
|
try { resizeTerm(); } catch {}
|
|
2058
2533
|
}
|
|
2059
2534
|
function bindSideHandle(handle, side, clickAction = null) {
|
|
2535
|
+
if (handle.getAttribute('role') === 'separator') handle.addEventListener('keydown', (event) => {
|
|
2536
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
2537
|
+
event.preventDefault();
|
|
2538
|
+
const step = event.shiftKey ? 40 : 16;
|
|
2539
|
+
const delta = (event.key === 'ArrowRight' ? 1 : -1) * (side === 'left' ? 1 : -1) * step;
|
|
2540
|
+
const width = event.key === 'Home' ? sideMin[side] : event.key === 'End' ? sideMax[side] : readSideWidth(side) + delta;
|
|
2541
|
+
appEl.classList.remove(side + '-collapsed');
|
|
2542
|
+
hccUi.safeSet('hcc.collapse.' + side, '0');
|
|
2543
|
+
setSideWidth(side, width);
|
|
2544
|
+
syncToggleIcons();
|
|
2545
|
+
syncPanelAccess();
|
|
2546
|
+
setTimeout(resizeTerm, 200);
|
|
2547
|
+
});
|
|
2060
2548
|
handle.addEventListener('pointerdown', (event) => beginSideDrag(side, event));
|
|
2061
2549
|
handle.addEventListener('pointermove', moveSideDrag);
|
|
2062
2550
|
handle.addEventListener('pointerup', endSideDrag);
|
|
@@ -2075,31 +2563,116 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
2075
2563
|
bindSideHandle(toggleRightBtn, 'right', toggleSide);
|
|
2076
2564
|
window.addEventListener('resize', () => {
|
|
2077
2565
|
applySideWidths();
|
|
2566
|
+
syncPanelAccess();
|
|
2567
|
+
closeActionsMenu();
|
|
2078
2568
|
setTimeout(() => { try { resizeTerm(); } catch {} }, 50);
|
|
2079
2569
|
});
|
|
2080
2570
|
applyCollapseState();
|
|
2081
2571
|
|
|
2082
2572
|
document.getElementById('stopBtn').addEventListener('click', () => {
|
|
2083
|
-
if (!active) return;
|
|
2573
|
+
if (!active || !canControl()) return;
|
|
2084
2574
|
const session = sessions.find((s) => s.id === active) || {};
|
|
2085
|
-
|
|
2575
|
+
if (session.type === 'native') return;
|
|
2576
|
+
document.getElementById('stopDialogTitle').textContent = tr(session.type === 'tmux' ? 'handoff.detach' : 'handoff.terminate') + ' ' + (sessionPeerId(session) || active) + '?';
|
|
2577
|
+
stopConfirmBtn.textContent = tr(session.type === 'tmux' ? 'handoff.detach' : 'handoff.terminate');
|
|
2086
2578
|
stopKillCb.checked = false;
|
|
2087
2579
|
const meta = sessionMetaText(session);
|
|
2088
|
-
document.getElementById('stopDialogMeta').textContent = meta
|
|
2580
|
+
document.getElementById('stopDialogMeta').textContent = [meta, tr(session.type === 'tmux' ? 'handoff.detachHelp' : 'handoff.terminateHelp')].filter(Boolean).join(' · ');
|
|
2581
|
+
stopKillCb.parentElement.parentElement.hidden = session.type !== 'tmux';
|
|
2089
2582
|
stopDialog._action = null; stopDialog._peerId = null;
|
|
2090
|
-
stopDialog.
|
|
2583
|
+
stopDialog._sessionId = active;
|
|
2584
|
+
stopDialog._projectRoot = currentProject;
|
|
2585
|
+
openDialog(stopDialog, stopCancelBtn);
|
|
2091
2586
|
});
|
|
2092
2587
|
document.getElementById('refreshBtn').addEventListener('click', () => {
|
|
2093
|
-
refreshVisibleData().catch(
|
|
2588
|
+
refreshVisibleData().catch(handleUiError);
|
|
2589
|
+
});
|
|
2590
|
+
document.getElementById('stateRetry').addEventListener('click', () => {
|
|
2591
|
+
refreshVisibleData().catch(handleUiError);
|
|
2094
2592
|
});
|
|
2095
2593
|
|
|
2594
|
+
function terminalFindAvailable() {
|
|
2595
|
+
const session = sessions.find(item => item.id === active);
|
|
2596
|
+
return Boolean(active && activeType === 'managed' && session && session.type !== 'app-server' && session.type !== 'native');
|
|
2597
|
+
}
|
|
2598
|
+
function focusSessionsSearch() {
|
|
2599
|
+
if (paneMode) { window.hccCommandPalette?.open(); return; }
|
|
2600
|
+
if (narrowViewport.matches) setMobileView('sessions');
|
|
2601
|
+
else {
|
|
2602
|
+
appEl.classList.remove('focus-mode');
|
|
2603
|
+
hccUi.safeSet('hcc.collapse.left', '0');
|
|
2604
|
+
applyCollapseState(); syncFocusButton();
|
|
2605
|
+
}
|
|
2606
|
+
document.getElementById('sessionSearch').focus();
|
|
2607
|
+
}
|
|
2608
|
+
const shortcutPrefix = /Mac|iPhone|iPad/.test(navigator.platform || '') ? '⌘ ⇧ ' : 'Ctrl Shift ';
|
|
2609
|
+
window.hccTerminalFindHost = {
|
|
2610
|
+
term, addon: searchAddon, available: terminalFindAvailable,
|
|
2611
|
+
subject: () => currentProject + ':' + (active || ''),
|
|
2612
|
+
showTerminal: () => { if (!paneMode) window.hccWorkspace?.primary(); setMobileView('terminal'); }
|
|
2613
|
+
};
|
|
2614
|
+
${terminalFindScript()}
|
|
2615
|
+
window.hccCommandHost = {
|
|
2616
|
+
openDialog, closeDialog, error: handleUiError,
|
|
2617
|
+
modalOpen: () => Boolean(document.querySelector('.dialog-overlay:not([hidden])')),
|
|
2618
|
+
terminalAvailable: terminalFindAvailable, focusSessions: focusSessionsSearch,
|
|
2619
|
+
commands() {
|
|
2620
|
+
const page = tr('commands.pages');
|
|
2621
|
+
const commands = [
|
|
2622
|
+
{ id:'new', label:tr('newSession'), group:page, enabled:!startPending, run:openStartDialog },
|
|
2623
|
+
{ id:'sessions', label:tr('sessionSearch'), group:page, shortcut:shortcutPrefix+'L', run:focusSessionsSearch },
|
|
2624
|
+
{ id:'find', label:tr('find.title'), group:page, shortcut:shortcutPrefix+'F', enabled:terminalFindAvailable(), run:() => window.hccTerminalFind.open() },
|
|
2625
|
+
{ id:'settings', label:tr('settings'), group:page, run:() => document.getElementById('settingsBtn').click() },
|
|
2626
|
+
{ id:'project-add', label:tr('addProject'), group:page, enabled:!document.getElementById('openProjectDialog').disabled, run:() => document.getElementById('openProjectDialog').click() },
|
|
2627
|
+
{ id:'refresh', label:tr('commands.refresh'), group:page, run:refreshVisibleData },
|
|
2628
|
+
{ id:'focus', label:tr(appEl.classList.contains('focus-mode') ? 'exitFocus' : 'focusMode'), group:page, enabled:!narrowViewport.matches, run:() => focusBtn.click() },
|
|
2629
|
+
{ id:'history', label:tr('aux.history'), group:page, run:() => document.getElementById('openHistoryDialog').click() },
|
|
2630
|
+
{ id:'review', label:tr('aux.review'), group:page, enabled:!document.getElementById('openReviewDialog').disabled, run:() => document.getElementById('openReviewDialog').click() }
|
|
2631
|
+
];
|
|
2632
|
+
for (const view of ['overview','tasks','messages','events']) {
|
|
2633
|
+
commands.push({ id:'view:'+view, label:tr('commands.view.'+view), group:page, run:() => {
|
|
2634
|
+
if (narrowViewport.matches) setMobileView('state');
|
|
2635
|
+
else { if (window.hccWorkspace?.isOpen) window.hccWorkspace.close(); appEl.classList.remove('focus-mode'); hccUi.safeSet('hcc.collapse.right','0'); applyCollapseState(); syncFocusButton(); }
|
|
2636
|
+
document.getElementById('stateTab-'+view).click(); document.getElementById('stateTab-'+view).focus();
|
|
2637
|
+
}});
|
|
2638
|
+
}
|
|
2639
|
+
for (const project of projects) {
|
|
2640
|
+
commands.push({ id:'project:'+project.root, label:project.name || project.root, detail:project.root, group:tr('commands.projects'), enabled:project.root!==currentProject, run:() => {
|
|
2641
|
+
document.getElementById('projectSelect').value = project.root;
|
|
2642
|
+
return switchProject(project.root);
|
|
2643
|
+
}});
|
|
2644
|
+
}
|
|
2645
|
+
const root = currentProject;
|
|
2646
|
+
for (const session of sessions) {
|
|
2647
|
+
commands.push({ id:'session:'+root+':'+session.id, label:session.name || session.task?.title || sessionPeerId(session), detail:sessionProvider(session)+' · '+statusText(session.status)+' · '+(session.cwd || ''), keywords:[session.id,sessionPeerId(session),session.task?.title,sessionRuntimeTarget(session),sessionProviderSessionValue(session),session.command].join(' '), group:tr('sessions'), enabled:session.status==='running', run:() => { if (currentProject===root) connectManaged(session.id); }});
|
|
2648
|
+
}
|
|
2649
|
+
for (const peer of detected) {
|
|
2650
|
+
commands.push({ id:'detected:'+root+':'+peer.id, label:peer.name || peer.id, detail:(peer.provider || peer.kind || '')+' · '+(peer.worktree || peer.cwd || ''), group:tr('detected'), run:() => { if (currentProject===root) connectDetected(peer.id); }});
|
|
2651
|
+
}
|
|
2652
|
+
if (paneMode) return commands.filter(command => ['new','sessions','find','settings','history','review','refresh'].includes(command.id) || command.id.startsWith('session:'));
|
|
2653
|
+
commands.splice(3,0,{id:'workspace',label:tr(window.hccWorkspace?.isOpen ? 'workspace.close' : 'workspace.split'),group:page,enabled:!document.getElementById('splitBtn').disabled,run:() => document.getElementById('splitBtn').click()});
|
|
2654
|
+
return commands;
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
${commandPaletteScript()}
|
|
2658
|
+
window.hccWorkspaceHost = {
|
|
2659
|
+
embedded:paneMode, app:appEl, primary:document.getElementById('workspacePrimaryPane'),
|
|
2660
|
+
project:() => currentProject, active:() => active, sessions:() => sessions, ready:() => sessionsLoaded,
|
|
2661
|
+
esc, showTerminal:() => setMobileView('terminal'), layoutChanged:() => { syncPanelAccess(); requestAnimationFrame(resizeTerm); }
|
|
2662
|
+
};
|
|
2663
|
+
${workspaceScript()}
|
|
2664
|
+
term.attachCustomKeyEventHandler(event => !event.defaultPrevented && !window.hccCommandPalette.captures(event));
|
|
2665
|
+
hccUi.bindControls();
|
|
2666
|
+
window.addEventListener('hcc:preferences', (event) => {
|
|
2667
|
+
if (event.detail.changed.some((key) => key === 'language' || key === 'resolvedLanguage')) applyLanguage();
|
|
2668
|
+
syncFocusButton();
|
|
2669
|
+
syncToggleIcons();
|
|
2670
|
+
applyTerminalPreferences();
|
|
2671
|
+
});
|
|
2672
|
+
applyTerminalPreferences();
|
|
2096
2673
|
applyLanguage();
|
|
2097
2674
|
loadProjects().then(() => Promise.all([refreshSessions(), refreshDetected(), refreshState()])).then(() => {
|
|
2098
|
-
|
|
2099
|
-
if (!active && !activeDetected) {
|
|
2100
|
-
const first = sessions.find(s => s.status === 'running');
|
|
2101
|
-
if (first) connectManaged(first.id);
|
|
2102
|
-
}
|
|
2675
|
+
if (!active && !activeDetected) restoreSelection();
|
|
2103
2676
|
}).catch((err) => {
|
|
2104
2677
|
connText('error');
|
|
2105
2678
|
console.error(err);
|
|
@@ -2112,24 +2685,25 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
2112
2685
|
refreshProjectsQuietly().catch(console.error);
|
|
2113
2686
|
}, 8000);
|
|
2114
2687
|
|
|
2115
|
-
stopCancelBtn.addEventListener('click', () =>
|
|
2116
|
-
stopDialog.addEventListener('click', (e) => { if (e.target === stopDialog) stopDialog.hidden = true; });
|
|
2117
|
-
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !stopDialog.hidden) stopDialog.hidden = true; });
|
|
2688
|
+
stopCancelBtn.addEventListener('click', () => closeDialog(stopDialog));
|
|
2118
2689
|
|
|
2119
2690
|
stopConfirmBtn.addEventListener('click', async () => {
|
|
2120
|
-
stopDialog
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2691
|
+
closeDialog(stopDialog);
|
|
2692
|
+
try {
|
|
2693
|
+
const killTmux = stopKillCb.checked;
|
|
2694
|
+
if (stopDialog._action === 'detected') {
|
|
2695
|
+
const peerId = stopDialog._peerId;
|
|
2696
|
+
stopDialog._action = null; stopDialog._peerId = null;
|
|
2697
|
+
if (!peerId) return;
|
|
2698
|
+
await api('/api/detected/' + encodeURIComponent(peerId) + '/stop', { method: 'POST', body: JSON.stringify({ kill_tmux: killTmux }) });
|
|
2699
|
+
await Promise.all([refreshSessions(), refreshDetected()]);
|
|
2700
|
+
} else {
|
|
2701
|
+
if (!active || !canControl()) return;
|
|
2702
|
+
if (stopDialog._sessionId !== active || stopDialog._projectRoot !== currentProject) throw new Error(tr('error.subject_changed'));
|
|
2703
|
+
await api('/api/sessions/' + encodeURIComponent(active) + '/stop', { method: 'POST', body: JSON.stringify({ kill_tmux: killTmux, action_token: sessionActionTokens.get(active) || '', epoch: controlEpoch() }) });
|
|
2704
|
+
await refreshSessions();
|
|
2705
|
+
}
|
|
2706
|
+
} catch (error) { handleUiError(error); }
|
|
2133
2707
|
});
|
|
2134
2708
|
</script>
|
|
2135
2709
|
</body>
|
|
@@ -2141,49 +2715,5 @@ export function webIndexHtml({ nonce } = {}) {
|
|
|
2141
2715
|
// flow is the token-in-URL exchange performed by the server; this form lets the
|
|
2142
2716
|
// user re-enter the token once to get a fresh cookie.
|
|
2143
2717
|
export function webLoginPage({ nonce } = {}) {
|
|
2144
|
-
|
|
2145
|
-
return `<!doctype html>
|
|
2146
|
-
<html lang="en">
|
|
2147
|
-
<head>
|
|
2148
|
-
<meta charset="utf-8">
|
|
2149
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2150
|
-
<title>hello-cc — sign in</title>
|
|
2151
|
-
<style>
|
|
2152
|
-
body { font-family: system-ui, sans-serif; background: #101418; color: #e8eaed; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
2153
|
-
.card { background: #1a2027; border: 1px solid #2c343d; border-radius: 10px; padding: 28px; width: 320px; }
|
|
2154
|
-
h1 { font-size: 18px; margin: 0 0 16px; }
|
|
2155
|
-
label { display: block; font-size: 13px; margin-bottom: 6px; color: #aab3bd; }
|
|
2156
|
-
input { width: 100%; box-sizing: border-box; padding: 9px 10px; border-radius: 6px; border: 1px solid #39424c; background: #101418; color: #e8eaed; font-size: 14px; }
|
|
2157
|
-
button { margin-top: 16px; width: 100%; padding: 9px; border-radius: 6px; border: 0; background: #40c4aa; color: #06211b; font-weight: 600; font-size: 14px; cursor: pointer; }
|
|
2158
|
-
button:hover { background: #57d4bc; }
|
|
2159
|
-
.err { color: #ff8b8b; font-size: 13px; margin-top: 10px; min-height: 18px; }
|
|
2160
|
-
</style>
|
|
2161
|
-
</head>
|
|
2162
|
-
<body>
|
|
2163
|
-
<div class="card">
|
|
2164
|
-
<h1>hello-cc</h1>
|
|
2165
|
-
<label for="tok">Access token</label>
|
|
2166
|
-
<input id="tok" type="password" autocomplete="off" placeholder="paste the ?token= value" autofocus>
|
|
2167
|
-
<button id="go">Sign in</button>
|
|
2168
|
-
<div class="err" id="err"></div>
|
|
2169
|
-
</div>
|
|
2170
|
-
<script${inlineScriptNonce}>
|
|
2171
|
-
const input = document.getElementById('tok');
|
|
2172
|
-
const err = document.getElementById('err');
|
|
2173
|
-
async function submit() {
|
|
2174
|
-
const token = input.value.trim();
|
|
2175
|
-
if (!token) return;
|
|
2176
|
-
err.textContent = '';
|
|
2177
|
-
const res = await fetch('/login', {
|
|
2178
|
-
method: 'POST',
|
|
2179
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2180
|
-
body: JSON.stringify({ token })
|
|
2181
|
-
});
|
|
2182
|
-
if (res.ok) { location.href = '/'; } else { err.textContent = 'Invalid token.'; }
|
|
2183
|
-
}
|
|
2184
|
-
document.getElementById('go').addEventListener('click', submit);
|
|
2185
|
-
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
|
2186
|
-
</script>
|
|
2187
|
-
</body>
|
|
2188
|
-
</html>`;
|
|
2718
|
+
return renderWebLoginPage({ inlineScriptNonce: nonceAttribute(nonce) });
|
|
2189
2719
|
}
|