@hncode/hncode 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/package.json +4 -2
  2. package/src/agent-task.js +154 -84
  3. package/src/agent.js +533 -145
  4. package/src/clipboard.js +5 -2
  5. package/src/colors.js +242 -238
  6. package/src/config.js +217 -17
  7. package/src/file-history.js +177 -0
  8. package/src/hooks.js +11 -1
  9. package/src/i18n.js +389 -0
  10. package/src/index.js +46 -3
  11. package/src/llm.js +139 -16
  12. package/src/permissions.js +120 -0
  13. package/src/session-hub.js +407 -0
  14. package/src/session.js +31 -0
  15. package/src/term.js +19 -3
  16. package/src/tool-result.js +236 -0
  17. package/src/tools/agent.js +11 -0
  18. package/src/tools/bash.js +84 -17
  19. package/src/tools/edit.js +192 -32
  20. package/src/tools/file-lines.js +5 -0
  21. package/src/tools/index.js +4 -0
  22. package/src/tools/memory.js +88 -0
  23. package/src/tools/read-media-file.js +85 -68
  24. package/src/tools/read.js +15 -1
  25. package/src/tools/utils.js +14 -0
  26. package/src/tools/write.js +35 -5
  27. package/src/tui.js +11665 -9179
  28. package/src/web-auth.js +98 -0
  29. package/src/web-daemon-cli.js +63 -0
  30. package/src/web-daemon-client.js +203 -0
  31. package/src/web-daemon.js +871 -0
  32. package/src/web-public/app.css +1509 -0
  33. package/src/web-public/app.js +2913 -0
  34. package/src/web-public/complete.js +154 -0
  35. package/src/web-public/fonts/JetBrainsMono-Bold.woff2 +0 -0
  36. package/src/web-public/fonts/JetBrainsMono-Regular.woff2 +0 -0
  37. package/src/web-public/fonts/noto-sans-sc-100-400-normal.woff2 +0 -0
  38. package/src/web-public/fonts/noto-sans-sc-101-400-normal.woff2 +0 -0
  39. package/src/web-public/fonts/noto-sans-sc-102-400-normal.woff2 +0 -0
  40. package/src/web-public/fonts/noto-sans-sc-103-400-normal.woff2 +0 -0
  41. package/src/web-public/fonts/noto-sans-sc-104-400-normal.woff2 +0 -0
  42. package/src/web-public/fonts/noto-sans-sc-105-400-normal.woff2 +0 -0
  43. package/src/web-public/fonts/noto-sans-sc-106-400-normal.woff2 +0 -0
  44. package/src/web-public/fonts/noto-sans-sc-107-400-normal.woff2 +0 -0
  45. package/src/web-public/fonts/noto-sans-sc-108-400-normal.woff2 +0 -0
  46. package/src/web-public/fonts/noto-sans-sc-109-400-normal.woff2 +0 -0
  47. package/src/web-public/fonts/noto-sans-sc-110-400-normal.woff2 +0 -0
  48. package/src/web-public/fonts/noto-sans-sc-111-400-normal.woff2 +0 -0
  49. package/src/web-public/fonts/noto-sans-sc-112-400-normal.woff2 +0 -0
  50. package/src/web-public/fonts/noto-sans-sc-113-400-normal.woff2 +0 -0
  51. package/src/web-public/fonts/noto-sans-sc-114-400-normal.woff2 +0 -0
  52. package/src/web-public/fonts/noto-sans-sc-115-400-normal.woff2 +0 -0
  53. package/src/web-public/fonts/noto-sans-sc-116-400-normal.woff2 +0 -0
  54. package/src/web-public/fonts/noto-sans-sc-117-400-normal.woff2 +0 -0
  55. package/src/web-public/fonts/noto-sans-sc-118-400-normal.woff2 +0 -0
  56. package/src/web-public/fonts/noto-sans-sc-119-400-normal.woff2 +0 -0
  57. package/src/web-public/fonts/noto-sans-sc-21-400-normal.woff2 +0 -0
  58. package/src/web-public/fonts/noto-sans-sc-22-400-normal.woff2 +0 -0
  59. package/src/web-public/fonts/noto-sans-sc-23-400-normal.woff2 +0 -0
  60. package/src/web-public/fonts/noto-sans-sc-24-400-normal.woff2 +0 -0
  61. package/src/web-public/fonts/noto-sans-sc-25-400-normal.woff2 +0 -0
  62. package/src/web-public/fonts/noto-sans-sc-26-400-normal.woff2 +0 -0
  63. package/src/web-public/fonts/noto-sans-sc-27-400-normal.woff2 +0 -0
  64. package/src/web-public/fonts/noto-sans-sc-28-400-normal.woff2 +0 -0
  65. package/src/web-public/fonts/noto-sans-sc-29-400-normal.woff2 +0 -0
  66. package/src/web-public/fonts/noto-sans-sc-30-400-normal.woff2 +0 -0
  67. package/src/web-public/fonts/noto-sans-sc-31-400-normal.woff2 +0 -0
  68. package/src/web-public/fonts/noto-sans-sc-32-400-normal.woff2 +0 -0
  69. package/src/web-public/fonts/noto-sans-sc-33-400-normal.woff2 +0 -0
  70. package/src/web-public/fonts/noto-sans-sc-34-400-normal.woff2 +0 -0
  71. package/src/web-public/fonts/noto-sans-sc-35-400-normal.woff2 +0 -0
  72. package/src/web-public/fonts/noto-sans-sc-36-400-normal.woff2 +0 -0
  73. package/src/web-public/fonts/noto-sans-sc-37-400-normal.woff2 +0 -0
  74. package/src/web-public/fonts/noto-sans-sc-38-400-normal.woff2 +0 -0
  75. package/src/web-public/fonts/noto-sans-sc-39-400-normal.woff2 +0 -0
  76. package/src/web-public/fonts/noto-sans-sc-4-400-normal.woff2 +0 -0
  77. package/src/web-public/fonts/noto-sans-sc-40-400-normal.woff2 +0 -0
  78. package/src/web-public/fonts/noto-sans-sc-41-400-normal.woff2 +0 -0
  79. package/src/web-public/fonts/noto-sans-sc-42-400-normal.woff2 +0 -0
  80. package/src/web-public/fonts/noto-sans-sc-43-400-normal.woff2 +0 -0
  81. package/src/web-public/fonts/noto-sans-sc-44-400-normal.woff2 +0 -0
  82. package/src/web-public/fonts/noto-sans-sc-45-400-normal.woff2 +0 -0
  83. package/src/web-public/fonts/noto-sans-sc-46-400-normal.woff2 +0 -0
  84. package/src/web-public/fonts/noto-sans-sc-47-400-normal.woff2 +0 -0
  85. package/src/web-public/fonts/noto-sans-sc-48-400-normal.woff2 +0 -0
  86. package/src/web-public/fonts/noto-sans-sc-49-400-normal.woff2 +0 -0
  87. package/src/web-public/fonts/noto-sans-sc-5-400-normal.woff2 +0 -0
  88. package/src/web-public/fonts/noto-sans-sc-50-400-normal.woff2 +0 -0
  89. package/src/web-public/fonts/noto-sans-sc-51-400-normal.woff2 +0 -0
  90. package/src/web-public/fonts/noto-sans-sc-52-400-normal.woff2 +0 -0
  91. package/src/web-public/fonts/noto-sans-sc-53-400-normal.woff2 +0 -0
  92. package/src/web-public/fonts/noto-sans-sc-54-400-normal.woff2 +0 -0
  93. package/src/web-public/fonts/noto-sans-sc-55-400-normal.woff2 +0 -0
  94. package/src/web-public/fonts/noto-sans-sc-56-400-normal.woff2 +0 -0
  95. package/src/web-public/fonts/noto-sans-sc-57-400-normal.woff2 +0 -0
  96. package/src/web-public/fonts/noto-sans-sc-58-400-normal.woff2 +0 -0
  97. package/src/web-public/fonts/noto-sans-sc-59-400-normal.woff2 +0 -0
  98. package/src/web-public/fonts/noto-sans-sc-6-400-normal.woff2 +0 -0
  99. package/src/web-public/fonts/noto-sans-sc-60-400-normal.woff2 +0 -0
  100. package/src/web-public/fonts/noto-sans-sc-61-400-normal.woff2 +0 -0
  101. package/src/web-public/fonts/noto-sans-sc-62-400-normal.woff2 +0 -0
  102. package/src/web-public/fonts/noto-sans-sc-63-400-normal.woff2 +0 -0
  103. package/src/web-public/fonts/noto-sans-sc-64-400-normal.woff2 +0 -0
  104. package/src/web-public/fonts/noto-sans-sc-65-400-normal.woff2 +0 -0
  105. package/src/web-public/fonts/noto-sans-sc-66-400-normal.woff2 +0 -0
  106. package/src/web-public/fonts/noto-sans-sc-67-400-normal.woff2 +0 -0
  107. package/src/web-public/fonts/noto-sans-sc-68-400-normal.woff2 +0 -0
  108. package/src/web-public/fonts/noto-sans-sc-69-400-normal.woff2 +0 -0
  109. package/src/web-public/fonts/noto-sans-sc-70-400-normal.woff2 +0 -0
  110. package/src/web-public/fonts/noto-sans-sc-71-400-normal.woff2 +0 -0
  111. package/src/web-public/fonts/noto-sans-sc-72-400-normal.woff2 +0 -0
  112. package/src/web-public/fonts/noto-sans-sc-73-400-normal.woff2 +0 -0
  113. package/src/web-public/fonts/noto-sans-sc-74-400-normal.woff2 +0 -0
  114. package/src/web-public/fonts/noto-sans-sc-75-400-normal.woff2 +0 -0
  115. package/src/web-public/fonts/noto-sans-sc-76-400-normal.woff2 +0 -0
  116. package/src/web-public/fonts/noto-sans-sc-77-400-normal.woff2 +0 -0
  117. package/src/web-public/fonts/noto-sans-sc-78-400-normal.woff2 +0 -0
  118. package/src/web-public/fonts/noto-sans-sc-79-400-normal.woff2 +0 -0
  119. package/src/web-public/fonts/noto-sans-sc-80-400-normal.woff2 +0 -0
  120. package/src/web-public/fonts/noto-sans-sc-81-400-normal.woff2 +0 -0
  121. package/src/web-public/fonts/noto-sans-sc-82-400-normal.woff2 +0 -0
  122. package/src/web-public/fonts/noto-sans-sc-83-400-normal.woff2 +0 -0
  123. package/src/web-public/fonts/noto-sans-sc-84-400-normal.woff2 +0 -0
  124. package/src/web-public/fonts/noto-sans-sc-85-400-normal.woff2 +0 -0
  125. package/src/web-public/fonts/noto-sans-sc-86-400-normal.woff2 +0 -0
  126. package/src/web-public/fonts/noto-sans-sc-87-400-normal.woff2 +0 -0
  127. package/src/web-public/fonts/noto-sans-sc-88-400-normal.woff2 +0 -0
  128. package/src/web-public/fonts/noto-sans-sc-89-400-normal.woff2 +0 -0
  129. package/src/web-public/fonts/noto-sans-sc-90-400-normal.woff2 +0 -0
  130. package/src/web-public/fonts/noto-sans-sc-91-400-normal.woff2 +0 -0
  131. package/src/web-public/fonts/noto-sans-sc-97-400-normal.woff2 +0 -0
  132. package/src/web-public/fonts/noto-sans-sc-98-400-normal.woff2 +0 -0
  133. package/src/web-public/fonts/noto-sans-sc-99-400-normal.woff2 +0 -0
  134. package/src/web-public/fonts/noto-sans-sc.css +775 -0
  135. package/src/web-public/fonts.css +31 -0
  136. package/src/web-public/home.css +222 -0
  137. package/src/web-public/home.html +33 -0
  138. package/src/web-public/home.js +160 -0
  139. package/src/web-public/i18n.js +983 -0
  140. package/src/web-public/index.html +196 -0
  141. package/src/web-public/login.css +123 -0
  142. package/src/web-public/login.html +25 -0
  143. package/src/web-public/login.js +46 -0
  144. package/src/web-public/settings.js +290 -0
  145. package/src/web-ws.js +226 -0
  146. package/src/web.js +380 -0
  147. package/src/tui.js.bak_20260921_191527 +0 -9203
@@ -0,0 +1,31 @@
1
+ /* Self-hosted faces: JetBrains Mono for everything, Noto Sans SC for CJK.
2
+
3
+ This product is Chinese-first. The UI body, the side rail, the settings, the
4
+ status line and the transcript are all Chinese — and JetBrains Mono has NO CJK
5
+ glyphs, so without Noto every Chinese character falls through to the OS font
6
+ and the page stops looking like one designed thing. Order matters: the mono
7
+ face comes first so Latin text (paths, code, numbers) gets the designed face,
8
+ then Noto's unicode-range shards take over for CJK.
9
+
10
+ There is deliberately no separate sans stack. kimi-code's UI is English, so it
11
+ can pair a Latin UI face (Geist) with a mono code face; a Chinese UI would be
12
+ all-Noto for the CJK anyway, so a second Latin face only ever covers the few
13
+ latin glyphs, at the cost of a second font and a mismatched metric. One mono
14
+ face that falls back to Noto reads as one thing. */
15
+ @import './fonts/noto-sans-sc.css';
16
+
17
+ @font-face {
18
+ font-family: 'JetBrains Mono';
19
+ font-style: normal;
20
+ font-weight: 400;
21
+ font-display: swap;
22
+ src: url('./fonts/JetBrainsMono-Regular.woff2') format('woff2');
23
+ }
24
+
25
+ @font-face {
26
+ font-family: 'JetBrains Mono';
27
+ font-style: normal;
28
+ font-weight: 700;
29
+ font-display: swap;
30
+ src: url('./fonts/JetBrainsMono-Bold.woff2') format('woff2');
31
+ }
@@ -0,0 +1,222 @@
1
+ /* hncode web UI — the session directory.
2
+ Same tokens as app.css and the same palette as the terminal (src/colors.js). */
3
+
4
+ :root {
5
+ color-scheme: dark;
6
+
7
+ --s0: #08090b;
8
+ --s1: #0d0f13;
9
+ --s2: #12151b;
10
+ --s3: #1a1e26;
11
+
12
+ --fg: #e6eaf2;
13
+ --fg1: #b6bfcf;
14
+ --fg2: #8f97a6;
15
+ --fg3: #6b7280;
16
+
17
+ --line: #1b202a;
18
+ --line2: #272d3a;
19
+
20
+ --teal: #00b8db;
21
+ --cyan: #00d7ff;
22
+ --red: #ff6b6b;
23
+ --green: #5cff8f;
24
+
25
+ --mono: "JetBrains Mono", "Noto Sans SC", ui-monospace, SFMono-Regular,
26
+ "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
27
+ --sans: var(--mono);
28
+
29
+ --radius: 3px;
30
+ }
31
+
32
+ * { box-sizing: border-box; }
33
+ [hidden] { display: none !important; }
34
+ html, body { margin: 0; }
35
+
36
+ body {
37
+ background: var(--s0);
38
+ color: var(--fg);
39
+ font-family: var(--sans);
40
+ font-size: 13px;
41
+ line-height: 1.5;
42
+ -webkit-font-smoothing: antialiased;
43
+ min-height: 100%;
44
+ }
45
+
46
+ * { scrollbar-width: thin; scrollbar-color: var(--line2) transparent; }
47
+ *::-webkit-scrollbar { width: 10px; height: 10px; }
48
+ *::-webkit-scrollbar-track { background: transparent; }
49
+ *::-webkit-scrollbar-thumb {
50
+ background: var(--line2);
51
+ border: 3px solid transparent;
52
+ background-clip: content-box;
53
+ border-radius: 5px;
54
+ }
55
+ ::selection { background: rgba(0, 184, 219, .28); }
56
+ :focus-visible { outline: 1px solid var(--teal); outline-offset: 2px; }
57
+
58
+ .page {
59
+ max-width: 980px;
60
+ margin: 0 auto;
61
+ padding: 40px 32px 80px;
62
+ }
63
+
64
+ .head {
65
+ display: flex;
66
+ align-items: flex-end;
67
+ justify-content: space-between;
68
+ gap: 24px;
69
+ padding-bottom: 16px;
70
+ border-bottom: 1px solid var(--line);
71
+ margin-bottom: 28px;
72
+ }
73
+ h1 {
74
+ font-family: var(--mono);
75
+ font-size: 20px;
76
+ font-weight: 500;
77
+ letter-spacing: -.01em;
78
+ margin: 0 0 8px;
79
+ color: var(--fg);
80
+ }
81
+ h1 .dim { color: var(--fg3); }
82
+ .lede { color: var(--fg2); margin: 0; max-width: 56ch; font-size: 12.5px; }
83
+ .head-right { display: flex; gap: 8px; flex-shrink: 0; }
84
+
85
+ .btn {
86
+ appearance: none;
87
+ font-family: var(--mono);
88
+ font-size: 11px;
89
+ line-height: 1.5;
90
+ padding: 4px 11px;
91
+ border: 1px solid var(--line2);
92
+ border-radius: var(--radius);
93
+ background: transparent;
94
+ color: var(--fg1);
95
+ cursor: pointer;
96
+ white-space: nowrap;
97
+ transition: border-color .12s ease, color .12s ease;
98
+ }
99
+ .btn:hover { border-color: var(--teal); color: var(--fg); }
100
+
101
+ .err {
102
+ color: var(--red);
103
+ border: 1px solid rgba(255, 107, 107, .4);
104
+ border-radius: var(--radius);
105
+ padding: 9px 12px;
106
+ margin: 0 0 24px;
107
+ font-family: var(--mono);
108
+ font-size: 11.5px;
109
+ }
110
+
111
+ .groups { display: flex; flex-direction: column; gap: 32px; }
112
+
113
+ .group-head {
114
+ display: flex;
115
+ align-items: baseline;
116
+ gap: 10px;
117
+ padding-bottom: 7px;
118
+ border-bottom: 1px solid var(--line);
119
+ margin-bottom: 4px;
120
+ }
121
+ .ws {
122
+ font-family: var(--mono);
123
+ font-size: 11.5px;
124
+ color: var(--fg1);
125
+ word-break: break-all;
126
+ }
127
+ .count {
128
+ margin-left: auto;
129
+ font-family: var(--mono);
130
+ font-size: 10.5px;
131
+ color: var(--fg3);
132
+ white-space: nowrap;
133
+ }
134
+
135
+ .rows { display: flex; flex-direction: column; }
136
+
137
+ .row {
138
+ display: grid;
139
+ grid-template-columns: minmax(0, 1fr) 110px 54px;
140
+ gap: 16px;
141
+ align-items: baseline;
142
+ padding: 9px 10px 9px 3px;
143
+ border-bottom: 1px solid var(--line);
144
+ border-left: 2px solid transparent;
145
+ text-decoration: none;
146
+ color: inherit;
147
+ transition: background-color .1s ease, border-color .1s ease;
148
+ }
149
+ .row:hover { background: var(--s1); border-left-color: var(--teal); }
150
+ .row:focus-visible { outline: 1px solid var(--teal); outline-offset: -2px; }
151
+
152
+ .row-title {
153
+ font-size: 13px;
154
+ color: var(--fg);
155
+ overflow: hidden;
156
+ text-overflow: ellipsis;
157
+ white-space: nowrap;
158
+ }
159
+ .row-id {
160
+ font-family: var(--mono);
161
+ font-size: 11px;
162
+ color: var(--fg3);
163
+ margin-left: 8px;
164
+ }
165
+ .row-when {
166
+ font-family: var(--mono);
167
+ font-size: 11px;
168
+ color: var(--fg3);
169
+ white-space: nowrap;
170
+ font-variant-numeric: tabular-nums;
171
+ }
172
+ .row-rounds {
173
+ font-family: var(--mono);
174
+ font-size: 11px;
175
+ color: var(--fg3);
176
+ text-align: right;
177
+ white-space: nowrap;
178
+ font-variant-numeric: tabular-nums;
179
+ }
180
+
181
+ .tag {
182
+ display: inline-block;
183
+ font-family: var(--mono);
184
+ font-size: 10px;
185
+ line-height: 15px;
186
+ padding: 0 5px;
187
+ border: 1px solid currentColor;
188
+ border-radius: 2px;
189
+ margin-left: 8px;
190
+ vertical-align: 1px;
191
+ }
192
+ .tag-live { color: var(--green); }
193
+ .tag-ro { color: var(--fg3); }
194
+
195
+ .empty {
196
+ color: var(--fg3);
197
+ padding: 32px 0;
198
+ font-family: var(--mono);
199
+ font-size: 12px;
200
+ }
201
+ @media (max-width: 720px) {
202
+ .page { padding: 28px 16px 60px; }
203
+ .head { flex-direction: column; align-items: flex-start; gap: 16px; }
204
+ .row { grid-template-columns: minmax(0, 1fr) auto; }
205
+ .row-rounds { display: none; }
206
+ }
207
+ /* Language switcher in the home header, mirroring the app's nav-lang. */
208
+ .nav-lang { display: flex; gap: 2px; flex: none; align-items: center; }
209
+ .nav-lang button {
210
+ appearance: none;
211
+ border: 0;
212
+ background: none;
213
+ color: var(--fg3);
214
+ font-family: var(--mono);
215
+ font-size: 11px;
216
+ padding: 1px 4px;
217
+ cursor: pointer;
218
+ border-radius: 2px;
219
+ transition: color .12s ease, background-color .12s ease;
220
+ }
221
+ .nav-lang button:hover { color: var(--fg1); }
222
+ .nav-lang button.is-on { color: var(--teal); }
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="robots" content="noindex, nofollow">
7
+ <title>hncode</title>
8
+ <link rel="stylesheet" href="fonts.css">
9
+ <link rel="stylesheet" href="/home.css">
10
+ </head>
11
+ <body>
12
+ <div class="page">
13
+
14
+ <header class="head">
15
+ <div class="head-left">
16
+ <h1>hncode <span class="dim" data-i18n="home.brandSuffix">sessions</span></h1>
17
+ <p class="lede" data-i18n="home.lede"></p>
18
+ </div>
19
+ <div class="head-right">
20
+ <div class="nav-lang" id="lang-switch"></div>
21
+ <button id="refresh" class="btn btn-quiet" data-i18n="home.refresh"></button>
22
+ <button id="logout" class="btn btn-quiet" data-i18n="home.logout"></button>
23
+ </div>
24
+ </header>
25
+
26
+ <p id="err" class="err" role="alert" hidden></p>
27
+
28
+ <main id="groups" class="groups"></main>
29
+
30
+ </div>
31
+ <script type="module" src="/home.js"></script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,160 @@
1
+
2
+
3
+ import { t, catalog, LANGUAGES, DEFAULT_LANG } from './i18n.js';
4
+
5
+ let LANG = DEFAULT_LANG;
6
+ try {
7
+ const saved = localStorage.getItem('hncode.lang');
8
+ if (saved && LANGUAGES.some((l) => l.id === saved)) LANG = saved;
9
+ } catch {}
10
+ let T = catalog(LANG);
11
+ const tr = (key, vars) => {
12
+ let s = T[key] || key;
13
+ if (vars) for (const k of Object.keys(vars)) s = s.split('{' + k + '}').join(String(vars[k]));
14
+ return s;
15
+ };
16
+
17
+ function applyStatic() {
18
+ document.documentElement.lang = LANG === 'zh' ? 'zh-CN' : 'en';
19
+ for (const el of document.querySelectorAll('[data-i18n]')) {
20
+ el.textContent = tr(el.getAttribute('data-i18n'));
21
+ }
22
+ }
23
+
24
+ // Language switcher in the header, mirroring the app's buildLangSwitch: one
25
+ // button per installed language; clicks update the module LANG, the localStorage
26
+ // preference (shared with the app so a choice here carries over) and re-render
27
+ // the static strings.
28
+ function buildLangSwitch() {
29
+ const box = document.getElementById('lang-switch');
30
+ if (!box) return;
31
+ box.replaceChildren();
32
+ box.title = tr('nav.language');
33
+ for (const l of LANGUAGES) {
34
+ const b = document.createElement('button');
35
+ b.type = 'button';
36
+ b.textContent = l.label;
37
+ b.className = l.id === LANG ? 'is-on' : '';
38
+ b.addEventListener('click', () => {
39
+ if (l.id === LANG) return;
40
+ LANG = l.id;
41
+ T = catalog(LANG);
42
+ try { localStorage.setItem('hncode.lang', LANG); } catch {}
43
+ buildLangSwitch();
44
+ applyStatic();
45
+ });
46
+ box.appendChild(b);
47
+ }
48
+ }
49
+
50
+ const groupsEl = document.getElementById('groups');
51
+ const errEl = document.getElementById('err');
52
+
53
+ function el(tag, cls, text) {
54
+ const n = document.createElement(tag);
55
+ if (cls) n.className = cls;
56
+ if (text != null) n.textContent = text;
57
+ return n;
58
+ }
59
+
60
+ function relTime(ms) {
61
+ if (!ms) return '';
62
+ const diff = Date.now() - ms;
63
+ if (diff < 0) return new Date(ms).toLocaleString();
64
+ const min = Math.floor(diff / 60000);
65
+ if (min < 1) return tr('home.justNow');
66
+ if (min < 60) return tr('home.minutesAgo', { n: min });
67
+ const hr = Math.floor(min / 60);
68
+ if (hr < 24) return tr('home.hoursAgo', { n: hr });
69
+ const day = Math.floor(hr / 24);
70
+ if (day < 7) return tr('home.daysAgo', { n: day });
71
+ return new Date(ms).toLocaleDateString();
72
+ }
73
+
74
+ function sessionRow(s, live) {
75
+ const a = el('a', 'row');
76
+ a.href = `/s/${encodeURIComponent(s.id)}/`;
77
+
78
+ const titleCell = el('div');
79
+ titleCell.appendChild(el('span', 'row-title', s.title || tr('home.untitled')));
80
+ titleCell.appendChild(el('span', 'row-id', s.id));
81
+ titleCell.appendChild(el('span', 'tag ' + (live ? 'tag-live' : 'tag-ro'),
82
+ live ? tr('home.live') : tr('home.readonly')));
83
+ a.appendChild(titleCell);
84
+
85
+ a.appendChild(el('div', 'row-when', relTime(s.at || s.updatedAt)));
86
+ a.appendChild(el('div', 'row-rounds', s.rounds ? String(s.rounds) : ''));
87
+ return a;
88
+ }
89
+
90
+ function render(live, saved) {
91
+ groupsEl.textContent = '';
92
+ const liveIds = new Set(live.map((s) => s.id));
93
+
94
+ const byWs = new Map();
95
+ const push = (ws, item) => {
96
+ const key = ws || tr('home.noWorkspace');
97
+ if (!byWs.has(key)) byWs.set(key, []);
98
+ byWs.get(key).push(item);
99
+ };
100
+ for (const s of live) push(s.workspace, { s, live: true });
101
+ for (const s of saved) {
102
+ if (liveIds.has(s.id)) continue;
103
+ push(s.workspace, { s, live: false });
104
+ }
105
+
106
+ if (!byWs.size) {
107
+ groupsEl.appendChild(el('p', 'empty', tr('home.empty')));
108
+ return;
109
+ }
110
+
111
+ const entries = [...byWs.entries()].map(([ws, items]) => ({
112
+ ws,
113
+ items,
114
+ anyLive: items.some((i) => i.live),
115
+ newest: Math.max(0, ...items.map((i) => i.s.at || i.s.updatedAt || 0)),
116
+ }));
117
+ entries.sort((a, b) => (b.anyLive - a.anyLive) || (b.newest - a.newest));
118
+
119
+ for (const g of entries) {
120
+ const sec = el('section', 'group');
121
+ const head = el('div', 'group-head');
122
+ head.appendChild(el('span', 'ws', g.ws));
123
+ head.appendChild(el('span', 'count', tr('home.sessionCount', { n: g.items.length })));
124
+ sec.appendChild(head);
125
+ const rows = el('div', 'rows');
126
+ for (const it of g.items) rows.appendChild(sessionRow(it.s, it.live));
127
+ sec.appendChild(rows);
128
+ groupsEl.appendChild(sec);
129
+ }
130
+ }
131
+
132
+ let timer = null;
133
+
134
+ async function load() {
135
+ try {
136
+ const res = await fetch('api/sessions');
137
+ if (res.status === 401) { location.replace('/login'); return; }
138
+ if (!res.ok) throw new Error('HTTP ' + res.status);
139
+ const data = await res.json();
140
+ errEl.hidden = true;
141
+ render(data.live || [], data.saved || []);
142
+ } catch (e) {
143
+ errEl.textContent = tr('home.loadFailed') + e.message;
144
+ errEl.hidden = false;
145
+ }
146
+ }
147
+
148
+ document.getElementById('refresh').addEventListener('click', () => { void load(); });
149
+
150
+ document.getElementById('logout').addEventListener('click', () => {
151
+ document.cookie = 'hncode_web=; Max-Age=0; Path=/';
152
+ location.replace('/login');
153
+ });
154
+
155
+ buildLangSwitch();
156
+ applyStatic();
157
+ void load();
158
+
159
+ timer = setInterval(() => { void load(); }, 5000);
160
+ window.addEventListener('beforeunload', () => { if (timer) clearInterval(timer); });