@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,196 @@
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="app.css">
10
+ </head>
11
+ <body>
12
+ <div class="app">
13
+
14
+ <aside class="rail" data-i18n-aria-label="rail.ariaLabel">
15
+ <div class="rail-search">
16
+ <input id="rail-filter" type="text" autocomplete="off" spellcheck="false"
17
+ data-i18n-placeholder="rail.search">
18
+ </div>
19
+ <div class="rail-meta">
20
+ <span id="rail-count" class="rail-count mono"></span>
21
+ <span class="rail-sep"></span>
22
+ <span id="conn" class="dot dot-off"></span>
23
+ <span id="conn-text" class="rail-conn"></span>
24
+ </div>
25
+ <div id="rail-list" class="rail-list"></div>
26
+ <div class="rail-foot">
27
+ <a class="rail-link" href="/" data-i18n="rail.all"></a>
28
+ <button id="btn-settings" class="rail-link" data-i18n="settings.open"></button>
29
+ <span class="rail-spacer"></span>
30
+ <div class="nav-lang" id="lang-switch"></div>
31
+ </div>
32
+ </aside>
33
+
34
+ <!-- Tapping outside the rail drawer closes it. -->
35
+ <div id="rail-scrim" class="rail-scrim"></div>
36
+
37
+ <main class="main">
38
+
39
+
40
+
41
+ <div class="sess-head">
42
+ <!-- On a narrow screen the rail becomes a drawer, so the header carries
43
+ the button that opens it. It is hidden while the rail is a permanent
44
+ column, where opening is already done. -->
45
+ <button id="btn-rail" class="icon-btn rail-toggle" data-i18n-title="rail.open">
46
+ <span aria-hidden="true">&#9776;</span>
47
+ </button>
48
+ <span id="sess-title" class="sess-title"></span>
49
+ <span id="sess-id" class="sess-id mono"></span>
50
+ <span id="readonly-badge" class="readonly-badge" data-i18n="nav.readonly" hidden></span>
51
+ <span class="sess-spacer"></span>
52
+ </div>
53
+
54
+ <nav class="tabs" data-i18n-aria-label="nav.ariaLabel">
55
+ <button class="tab is-on" data-view="chat" data-i18n="nav.chat"></button>
56
+ <button class="tab" data-view="tasks" data-i18n="nav.tasks"></button>
57
+ <button class="tab" data-view="files" data-i18n="nav.files"></button>
58
+ <button class="tab" data-view="tools" data-i18n="nav.tools"></button>
59
+ <button class="tab" data-view="commands" data-i18n="nav.commands"></button>
60
+ <button class="tab" data-view="logs" data-i18n="nav.logs"></button>
61
+ <span class="tab-spacer"></span>
62
+
63
+ <span id="view-sub" class="tab-sub"></span>
64
+ </nav>
65
+
66
+ <div class="body">
67
+ <section id="view-chat" class="view">
68
+ <div id="stream" class="stream" tabindex="0"></div>
69
+ </section>
70
+
71
+ <section id="view-tasks" class="view" hidden>
72
+ <div id="tasks" class="cards"></div>
73
+ </section>
74
+
75
+ <section id="view-files" class="view" hidden>
76
+ <div id="files" class="cards"></div>
77
+ </section>
78
+
79
+ <section id="view-tools" class="view" hidden>
80
+ <div id="tools" class="cards"></div>
81
+ </section>
82
+
83
+ <section id="view-commands" class="view" hidden>
84
+ <div id="commands" class="cards"></div>
85
+ </section>
86
+
87
+ <section id="view-logs" class="view" hidden>
88
+ <div class="logs-bar">
89
+ <button id="logs-clear" class="btn-quiet" data-i18n="logs.clear"></button>
90
+ </div>
91
+ <div id="logs" class="logs"></div>
92
+ </section>
93
+ </div>
94
+
95
+ <div class="stack">
96
+ <!-- Working heads the work area: what the agent is doing right now is the
97
+ most current thing, so it sits above the todo/queue panels and the
98
+ composer. When there is nothing else, it is the only row above input. -->
99
+ <div id="working" class="working" hidden><span id="working-text"></span></div>
100
+
101
+ <!-- Each panel's top edge is a rule that doubles as a DRAG HANDLE, and its
102
+ heading is the panel's TITLE (cyan, bold) — both matching the terminal,
103
+ which draws `───` then ` Todo` in `C.cyan + C.bold` and marks that
104
+ rule as the resize hitbox. -->
105
+ <div id="todo-panel" class="stack-panel" hidden>
106
+ <div class="stack-rule" data-resize="todo" role="separator" aria-orientation="horizontal"></div>
107
+ <span class="stack-label" data-i18n="side.todos"></span>
108
+ <ul id="st-todos" class="todos"></ul>
109
+ </div>
110
+ <div id="queue-panel" class="stack-panel" hidden>
111
+ <div class="stack-rule" data-resize="queue" role="separator" aria-orientation="horizontal"></div>
112
+ <span class="stack-label" data-i18n="side.queue"></span>
113
+ <ul id="st-queue" class="todos"></ul>
114
+ </div>
115
+ <div id="complete" class="complete" hidden></div>
116
+
117
+ <form id="composer" class="composer">
118
+ <span id="pmode" class="pmode" data-i18n-title="chat.shellHint">&rsaquo;</span>
119
+ <textarea id="input" rows="1" spellcheck="false" data-i18n-placeholder="chat.placeholder"></textarea>
120
+ <!-- Interrupt lives next to the composer, not in the header. It is the
121
+ action you reach for WHILE a turn runs, and the composer is where
122
+ you already are; the header put it a screen away from the thing it
123
+ interrupts. It replaces Send while busy, so the two never compete
124
+ for the same edge. -->
125
+ <button type="button" id="btn-interrupt" class="btn is-stop" data-i18n="chat.interrupt" hidden></button>
126
+ <button type="submit" id="send" class="btn" data-i18n="chat.send"></button>
127
+ </form>
128
+
129
+
130
+ <div id="statusline" class="statusline"></div>
131
+ </div>
132
+ </main>
133
+ </div>
134
+
135
+ <div id="popover" class="popover" hidden>
136
+ <div class="popover-title" id="popover-title"></div>
137
+ <div class="popover-list" id="popover-list"></div>
138
+ </div>
139
+
140
+ <div id="modal" class="modal" hidden>
141
+ <div class="modal-box" id="modal-box" role="dialog" aria-modal="true">
142
+ <div id="modal-title" class="modal-title"></div>
143
+ <div id="modal-body" class="modal-body"></div>
144
+ <div id="modal-actions" class="modal-actions"></div>
145
+ </div>
146
+ </div>
147
+
148
+ <div id="settings" class="overlay" hidden>
149
+ <div class="overlay-box" role="dialog" aria-modal="true" aria-labelledby="settings-title">
150
+ <header class="overlay-head">
151
+ <h2 id="settings-title" data-i18n="settings.title"></h2>
152
+ <button id="settings-close" class="btn-quiet" data-i18n="settings.close"></button>
153
+ </header>
154
+ <div class="overlay-body">
155
+ <section class="set-group">
156
+ <h3 data-i18n="settings.session"></h3>
157
+ <div class="set-row">
158
+ <label for="set-title" data-i18n="settings.titleLabel"></label>
159
+ <input id="set-title" type="text" autocomplete="off" spellcheck="false">
160
+ <button id="set-title-save" class="btn-quiet" data-i18n="settings.save"></button>
161
+ </div>
162
+ <p id="set-session-id" class="muted mono"></p>
163
+ </section>
164
+ <section class="set-group">
165
+ <h3 data-i18n="settings.providers"></h3>
166
+ <div id="provider-list" class="set-list"></div>
167
+ <div class="set-row">
168
+ <input id="np-name" type="text" placeholder="name" data-i18n-placeholder="settings.phName" autocomplete="off" spellcheck="false">
169
+ <input id="np-url" type="text" placeholder="base URL" data-i18n-placeholder="settings.phUrl" autocomplete="off" spellcheck="false">
170
+ <input id="np-key" type="password" placeholder="API key" data-i18n-placeholder="settings.phKey" autocomplete="off">
171
+ <select id="np-proto"><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option></select>
172
+ <button id="np-add" class="btn" data-i18n="settings.addProvider"></button>
173
+ </div>
174
+ <div class="set-row">
175
+ <button id="imp-known" class="btn-quiet" data-i18n="settings.importKnown"></button>
176
+ <input id="imp-filter" type="text" placeholder="filter" data-i18n-placeholder="settings.phFilter" autocomplete="off" spellcheck="false">
177
+ </div>
178
+ <div id="known-list" class="set-list" hidden></div>
179
+ </section>
180
+ <section class="set-group">
181
+ <h3 data-i18n="settings.prefs"></h3>
182
+ <div id="pref-list" class="set-list"></div>
183
+ </section>
184
+ <section class="set-group">
185
+ <h3 data-i18n="settings.info"></h3>
186
+ <dl id="cfg-view" class="kv"></dl>
187
+ </section>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <div id="notice" class="notice" hidden></div>
193
+
194
+ <script type="module" src="app.js"></script>
195
+ </body>
196
+ </html>
@@ -0,0 +1,123 @@
1
+ /* hncode web UI — the token gate.
2
+ Same tokens as app.css; the point is that the sign-in page belongs to the
3
+ same product as the thing it unlocks. */
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+
8
+ --s0: #08090b;
9
+ --s1: #0d0f13;
10
+
11
+ --fg: #e6eaf2;
12
+ --fg1: #b6bfcf;
13
+ --fg2: #8f97a6;
14
+ --fg3: #6b7280;
15
+
16
+ --line: #1b202a;
17
+ --line2: #272d3a;
18
+
19
+ --teal: #00b8db;
20
+ --red: #ff6b6b;
21
+
22
+ --mono: "JetBrains Mono", "Noto Sans SC", ui-monospace, SFMono-Regular,
23
+ "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
24
+ --sans: var(--mono);
25
+
26
+ --radius: 3px;
27
+ }
28
+
29
+ * { box-sizing: border-box; }
30
+ [hidden] { display: none !important; }
31
+ html, body { margin: 0; height: 100%; }
32
+
33
+ body {
34
+ background: var(--s0);
35
+ color: var(--fg);
36
+ font-family: var(--sans);
37
+ font-size: 13px;
38
+ line-height: 1.5;
39
+ -webkit-font-smoothing: antialiased;
40
+ }
41
+
42
+ ::selection { background: rgba(0, 184, 219, .28); }
43
+ :focus-visible { outline: 1px solid var(--teal); outline-offset: 2px; }
44
+
45
+ .gate {
46
+ max-width: 380px;
47
+ margin: 0 auto;
48
+ padding: 22vh 28px 0;
49
+ }
50
+
51
+ h1 {
52
+ font-family: var(--mono);
53
+ font-size: 20px;
54
+ font-weight: 500;
55
+ letter-spacing: -.01em;
56
+ margin: 0 0 10px;
57
+ color: var(--fg);
58
+ }
59
+
60
+ .lede {
61
+ color: var(--fg2);
62
+ margin: 0 0 28px;
63
+ font-size: 12.5px;
64
+ max-width: 44ch;
65
+ }
66
+
67
+ form { display: flex; flex-direction: column; gap: 0; }
68
+
69
+ label {
70
+ font-family: var(--mono);
71
+ font-size: 10.5px;
72
+ color: var(--fg3);
73
+ margin-bottom: 6px;
74
+ }
75
+
76
+ input {
77
+ font-family: var(--mono);
78
+ font-size: 12.5px;
79
+ color: var(--fg);
80
+ background: var(--s1);
81
+ border: 1px solid var(--line2);
82
+ border-radius: var(--radius);
83
+ padding: 8px 10px;
84
+ width: 100%;
85
+ transition: border-color .12s ease;
86
+ }
87
+ input:focus { outline: none; border-color: var(--teal); }
88
+ input::placeholder { color: var(--fg3); }
89
+
90
+ button {
91
+ font-family: var(--mono);
92
+ font-size: 11.5px;
93
+ margin-top: 12px;
94
+ padding: 7px 14px;
95
+ color: var(--fg1);
96
+ background: transparent;
97
+ border: 1px solid var(--line2);
98
+ border-radius: var(--radius);
99
+ cursor: pointer;
100
+ align-self: flex-start;
101
+ min-width: 104px;
102
+ transition: border-color .12s ease, color .12s ease;
103
+ }
104
+ button:hover { border-color: var(--teal); color: var(--fg); }
105
+ button:disabled { color: var(--fg3); border-color: var(--line); cursor: not-allowed; }
106
+
107
+ .err {
108
+ color: var(--red);
109
+ margin: 16px 0 0;
110
+ font-family: var(--mono);
111
+ font-size: 11.5px;
112
+ }
113
+
114
+ .foot {
115
+ color: var(--fg3);
116
+ margin: 34px 0 0;
117
+ font-size: 11.5px;
118
+ line-height: 1.7;
119
+ }
120
+
121
+ @media (max-width: 480px) {
122
+ .gate { padding-top: 14vh; }
123
+ }
@@ -0,0 +1,25 @@
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="/login.css">
10
+ </head>
11
+ <body>
12
+ <main class="gate">
13
+ <h1 data-i18n="login.heading">hncode</h1>
14
+ <p class="lede" data-i18n="login.lede"></p>
15
+ <form id="f">
16
+ <label for="t" data-i18n="login.tokenLabel"></label>
17
+ <input id="t" name="token" type="password" autocomplete="off" spellcheck="false" autofocus>
18
+ <button type="submit" data-i18n="login.submit"></button>
19
+ </form>
20
+ <p id="err" class="err" role="alert" hidden></p>
21
+ <p class="foot" data-i18n="login.foot"></p>
22
+ </main>
23
+ <script type="module" src="/login.js"></script>
24
+ </body>
25
+ </html>
@@ -0,0 +1,46 @@
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
+ const T = catalog(LANG);
11
+ const tr = (key) => T[key] || key;
12
+
13
+ document.documentElement.lang = LANG === 'zh' ? 'zh-CN' : 'en';
14
+ for (const el of document.querySelectorAll('[data-i18n]')) {
15
+ el.textContent = tr(el.getAttribute('data-i18n'));
16
+ }
17
+
18
+ const form = document.getElementById('f');
19
+ const input = document.getElementById('t');
20
+ const err = document.getElementById('err');
21
+
22
+ function fail(msg) {
23
+ err.textContent = msg;
24
+ err.hidden = false;
25
+ }
26
+
27
+ form.addEventListener('submit', async (ev) => {
28
+ ev.preventDefault();
29
+ err.hidden = true;
30
+ const btn = form.querySelector('button');
31
+ btn.disabled = true;
32
+ try {
33
+ const res = await fetch('/api/login', {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json' },
36
+ body: JSON.stringify({ token: input.value.trim() }),
37
+ });
38
+ if (res.ok) { location.replace('/'); return; }
39
+ const body = await res.json().catch(() => ({}));
40
+ fail(body.error === 'invalid token' ? tr('login.badToken') : (body.error || tr('login.failed')));
41
+ } catch (e) {
42
+ fail(tr('login.unreachable') + e.message);
43
+ } finally {
44
+ btn.disabled = false;
45
+ }
46
+ });
@@ -0,0 +1,290 @@
1
+
2
+
3
+ const $ = (id) => document.getElementById(id);
4
+
5
+ export class SettingsPanel {
6
+
7
+ constructor(opts) {
8
+ this.act = opts.act;
9
+ this.getStatus = opts.getStatus;
10
+ this.tr = opts.tr;
11
+ this.toast = opts.toast || (() => {});
12
+ this.providers = [];
13
+ this.known = [];
14
+ this.loaded = false;
15
+ }
16
+
17
+ get open() { return !$('settings').hidden; }
18
+
19
+ show() {
20
+ $('settings').hidden = false;
21
+ const st = this.getStatus() || {};
22
+ $('set-title').value = st.title || '';
23
+ $('set-session-id').textContent = st.session || '';
24
+
25
+ void this.refreshProviders();
26
+ void this.refreshConfig();
27
+ this.renderPrefs();
28
+ }
29
+
30
+ hide() { $('settings').hidden = true; }
31
+
32
+ async refreshProviders() {
33
+ const r = await this.act('listProviders', []);
34
+ this.providers = (r && r.result) || [];
35
+ this.renderProviders();
36
+ }
37
+
38
+ renderProviders() {
39
+ const box = $('provider-list');
40
+ box.textContent = '';
41
+ if (!this.providers.length) {
42
+ const p = document.createElement('p');
43
+ p.className = 'muted';
44
+ p.textContent = this.tr('settings.noProviders');
45
+ box.appendChild(p);
46
+ return;
47
+ }
48
+ for (const p of this.providers) {
49
+ const row = document.createElement('div');
50
+ row.className = 'set-item';
51
+
52
+ const name = document.createElement('span');
53
+ name.className = 'set-name';
54
+ name.textContent = p.name;
55
+ row.appendChild(name);
56
+
57
+ const tag = document.createElement('span');
58
+ tag.className = 'set-tag' + (p.keySet ? '' : ' off');
59
+ tag.textContent = p.keySet ? this.tr('settings.tagKey') : this.tr('settings.tagNoKey');
60
+ row.appendChild(tag);
61
+
62
+ const sub = document.createElement('span');
63
+ sub.className = 'set-sub';
64
+ sub.textContent = `${p.protocol} · ${p.baseUrl || this.tr('settings.noUrl')} · ${this.tr('settings.modelCount', { n: p.models.length })}`;
65
+ sub.title = p.baseUrl || '';
66
+ row.appendChild(sub);
67
+
68
+ const disc = document.createElement('button');
69
+ disc.className = 'set-act';
70
+ disc.type = 'button';
71
+ disc.textContent = this.tr('settings.discover');
72
+ disc.addEventListener('click', async () => {
73
+ disc.disabled = true;
74
+ try {
75
+ const r = await this.act('discoverModels', [p.name]);
76
+ const n = (r && r.result && r.result.found) || 0;
77
+ this.toast(this.tr('settings.foundModels', { n }), 'info');
78
+ await this.refreshProviders();
79
+ } catch (e) {
80
+ this.toast(this.tr('settings.failed') + e.message, 'error');
81
+ } finally {
82
+ disc.disabled = false;
83
+ }
84
+ });
85
+ row.appendChild(disc);
86
+
87
+ const del = document.createElement('button');
88
+ del.className = 'set-act danger';
89
+ del.type = 'button';
90
+ del.textContent = this.tr('settings.remove');
91
+ del.addEventListener('click', async () => {
92
+ if (!confirm(this.tr('settings.confirmRemove', { name: p.name }))) return;
93
+ try {
94
+ await this.act('removeProvider', [p.name]);
95
+ this.toast(this.tr('settings.removed', { name: p.name }), 'info');
96
+ await this.refreshProviders();
97
+ } catch (e) {
98
+ this.toast(this.tr('settings.failed') + e.message, 'error');
99
+ }
100
+ });
101
+ row.appendChild(del);
102
+ box.appendChild(row);
103
+ }
104
+ }
105
+
106
+ async addProvider() {
107
+ const name = $('np-name').value.trim();
108
+ const url = $('np-url').value.trim();
109
+ const key = $('np-key').value;
110
+ const proto = $('np-proto').value;
111
+ if (!name) { this.toast(this.tr('settings.nameRequired'), 'error'); return; }
112
+ try {
113
+ await this.act('addProvider', [name, url, key, proto]);
114
+ $('np-name').value = '';
115
+ $('np-url').value = '';
116
+ $('np-key').value = '';
117
+ this.toast(this.tr('settings.added', { name }), 'info');
118
+ await this.refreshProviders();
119
+ await this.refreshConfig();
120
+ } catch (e) {
121
+ this.toast(this.tr('settings.failed') + e.message, 'error');
122
+ }
123
+ }
124
+
125
+ async toggleKnown() {
126
+ const box = $('known-list');
127
+ if (!box.hidden) { box.hidden = true; return; }
128
+ if (!this.known.length) {
129
+ try {
130
+ this.known = (await this.act('listKnownProviders', [])).result || [];
131
+ } catch (e) {
132
+ this.toast(this.tr('settings.failed') + e.message, 'error');
133
+ return;
134
+ }
135
+ }
136
+ box.hidden = false;
137
+ this.renderKnown('');
138
+ }
139
+
140
+ renderKnown(filter) {
141
+ const box = $('known-list');
142
+ box.textContent = '';
143
+ const q = String(filter || '').toLowerCase();
144
+ const hits = this.known.filter((p) => !q || p.id.toLowerCase().includes(q) || p.name.toLowerCase().includes(q));
145
+ if (!hits.length) {
146
+ const p = document.createElement('p');
147
+ p.className = 'muted';
148
+ p.textContent = this.tr('settings.noMatch');
149
+ box.appendChild(p);
150
+ return;
151
+ }
152
+ for (const p of hits.slice(0, 60)) {
153
+ const row = document.createElement('div');
154
+ row.className = 'set-item';
155
+ const name = document.createElement('span');
156
+ name.className = 'set-name';
157
+ name.textContent = p.name;
158
+ row.appendChild(name);
159
+ const sub = document.createElement('span');
160
+ sub.className = 'set-sub';
161
+ sub.textContent = p.api;
162
+ sub.title = p.api;
163
+ row.appendChild(sub);
164
+ const add = document.createElement('button');
165
+ add.className = 'set-act';
166
+ add.type = 'button';
167
+ add.textContent = this.tr('settings.import');
168
+ add.addEventListener('click', async () => {
169
+ const key = prompt(this.tr('settings.keyPrompt', { name: p.name }), '');
170
+ if (key === null) return;
171
+ add.disabled = true;
172
+ try {
173
+ const r = await this.act('importKnownProvider', [p.id, p.id, key]);
174
+ const n = (r && r.result && r.result.added) || 0;
175
+ this.toast(this.tr('settings.imported', { name: p.name, n }), 'info');
176
+ await this.refreshProviders();
177
+ await this.refreshConfig();
178
+ } catch (e) {
179
+ this.toast(this.tr('settings.failed') + e.message, 'error');
180
+ } finally {
181
+ add.disabled = false;
182
+ }
183
+ });
184
+ row.appendChild(add);
185
+ box.appendChild(row);
186
+ }
187
+ }
188
+
189
+ renderPrefs() {
190
+ const box = $('pref-list');
191
+
192
+ if (!this.cfgKeys) this.cfgKeys = [];
193
+ box.textContent = '';
194
+ const PREFS = [
195
+ ['calm_mode', 'pref.calm'],
196
+ ['auto_compact', 'pref.autoCompact'],
197
+ ['auto_update', 'pref.autoUpdate'],
198
+ ['prompt_cache', 'pref.promptCache'],
199
+ ['tool_allow_external_paths', 'pref.external'],
200
+ ];
201
+ for (const [key, label] of PREFS) {
202
+ const row = document.createElement('div');
203
+ row.className = 'set-item';
204
+ const name = document.createElement('span');
205
+ name.className = 'set-name';
206
+ name.textContent = this.tr(label);
207
+ row.appendChild(name);
208
+ const sel = document.createElement('select');
209
+ sel.className = 'set-select';
210
+ for (const v of ['on', 'off']) {
211
+ const o = document.createElement('option');
212
+ o.value = v;
213
+ o.textContent = v;
214
+ sel.appendChild(o);
215
+ }
216
+ const cur = String(this.cfgKeys[key] ? 'on' : 'off');
217
+ sel.value = cur;
218
+ sel.addEventListener('change', async () => {
219
+ try {
220
+ await this.act('setConfig', [key, sel.value === 'on' ? 'true' : 'false']);
221
+ this.toast(`${this.tr(label)} → ${sel.value}`, 'info');
222
+ await this.refreshConfig();
223
+ } catch (e) {
224
+ this.toast(this.tr('settings.failed') + e.message, 'error');
225
+ sel.value = cur;
226
+ }
227
+ });
228
+ row.appendChild(sel);
229
+ box.appendChild(row);
230
+ }
231
+ }
232
+
233
+ async refreshConfig() {
234
+ try {
235
+ const r = await this.act('getConfig', []);
236
+ const c = (r && r.result) || {};
237
+ this.cfgKeys = c;
238
+ const dl = $('cfg-view');
239
+ dl.textContent = '';
240
+ const rows = [
241
+ ['settings.model', c.model],
242
+ ['settings.provider', c.provider],
243
+ ['settings.protocol', c.protocol],
244
+ ['settings.endpoint', c.endpoint],
245
+ ['settings.key', c.apiKeySet ? this.tr('settings.valSet') : this.tr('settings.valNotSet')],
246
+ ['settings.workspace', c.workspace],
247
+ ['settings.ctx', String(c.maxContextTokens || '')],
248
+ ];
249
+ for (const [k, v] of rows) {
250
+ const dt = document.createElement('dt');
251
+ dt.textContent = this.tr(k);
252
+ const dd = document.createElement('dd');
253
+ dd.className = 'mono ellip';
254
+ dd.textContent = v == null || v === '' ? '—' : String(v);
255
+ dd.title = dd.textContent;
256
+ dl.append(dt, dd);
257
+ }
258
+ this.renderPrefs();
259
+ } catch (e) {
260
+ this.toast(this.tr('settings.failed') + e.message, 'error');
261
+ }
262
+ }
263
+
264
+ async saveTitle() {
265
+ const t = $('set-title').value.trim();
266
+ if (!t) { this.toast(this.tr('settings.nameRequired'), 'error'); return; }
267
+ try {
268
+ await this.act('setTitle', [t]);
269
+ this.toast(this.tr('settings.titleSaved'), 'info');
270
+ } catch (e) {
271
+ this.toast(this.tr('settings.failed') + e.message, 'error');
272
+ }
273
+ }
274
+ }
275
+
276
+ export function attachSettings(panel) {
277
+ $('btn-settings').addEventListener('click', () => panel.show());
278
+ $('settings-close').addEventListener('click', () => panel.hide());
279
+
280
+ $('settings').addEventListener('mousedown', (ev) => {
281
+ if (ev.target === $('settings')) panel.hide();
282
+ });
283
+ document.addEventListener('keydown', (ev) => {
284
+ if (ev.key === 'Escape' && panel.open) panel.hide();
285
+ });
286
+ $('set-title-save').addEventListener('click', () => { void panel.saveTitle(); });
287
+ $('np-add').addEventListener('click', () => { void panel.addProvider(); });
288
+ $('imp-known').addEventListener('click', () => { void panel.toggleKnown(); });
289
+ $('imp-filter').addEventListener('input', () => panel.renderKnown($('imp-filter').value));
290
+ }