@hncode/hncode 0.3.6 → 0.4.1

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 (148) hide show
  1. package/package.json +5 -3
  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/bin/{install.js → install.cjs} +0 -0
  148. /package/bin/{preinstall.js → preinstall.cjs} +0 -0
package/src/i18n.js ADDED
@@ -0,0 +1,389 @@
1
+ // UI strings for the web interface — the ONE place they are defined.
2
+ //
3
+ // Both sides read this file: the browser imports it as a plain script (see
4
+ // web-public/i18n.js, which is generated from this module so the two can never
5
+ // drift), and the server uses it for the few strings it returns to the client
6
+ // (error messages, the login page).
7
+ // drift), and the server uses it for the few strings it returns to the client
8
+ // (error messages, the login page).
9
+ //
10
+ // Adding a language: add a key to every entry in `MESSAGES`. A missing key falls
11
+ // back to English at lookup time rather than printing the key, so a partly
12
+ // translated language degrades readably instead of showing `nav.chat`.
13
+
14
+ export const LANGUAGES = [
15
+ { id: 'zh', label: '中文' },
16
+ { id: 'en', label: 'English' },
17
+ ];
18
+
19
+ // The WEB UI's default. The terminal never reads this file: TUI copy is English
20
+ // by construction. A browser keeps its choice in localStorage (`hncode.lang`), so
21
+ // this only decides the FIRST visit — and English is the safer default there,
22
+ // since a link can be opened by anyone.
23
+ export const DEFAULT_LANG = 'en';
24
+
25
+ export const MESSAGES = {
26
+ // ---- document / login ----------------------------------------------------
27
+ 'app.title': { zh: 'hncode', en: 'hncode' },
28
+ 'login.heading': { zh: 'hncode', en: 'hncode' },
29
+ 'login.lede': {
30
+ zh: '访问令牌保存在 config.toml 里,第一次运行时自动生成,之后一直不变。',
31
+ en: 'The access token lives in config.toml. It is generated once and stays the same.',
32
+ },
33
+ 'login.tokenLabel': { zh: '访问令牌', en: 'Access token' },
34
+ 'login.submit': { zh: '进入', en: 'Sign in' },
35
+ 'login.badToken': { zh: '令牌不对。', en: 'Invalid token.' },
36
+ 'login.failed': { zh: '登录失败。', en: 'Sign-in failed.' },
37
+ 'login.unreachable': { zh: '连不上服务:', en: 'Cannot reach the server: ' },
38
+ 'login.foot': {
39
+ zh: '本地服务,只监听回环地址。在任意一个终端里运行 /web 都能看到这台机器上的所有会话。',
40
+ en: 'Local service, loopback only. Run /web in any terminal to see every session on this machine.',
41
+ },
42
+
43
+ // ---- home (the session directory) ---------------------------------------
44
+ 'home.brandSuffix': { zh: '会话', en: 'sessions' },
45
+ 'home.lede': {
46
+ zh: '这台机器上的所有会话。运行中的可以继续对话,已退出的只能查看。',
47
+ en: 'Every session on this machine. Running ones accept input; exited ones are read-only.',
48
+ },
49
+ 'home.empty': {
50
+ zh: '还没有任何会话。在终端里运行 hncode 开始一个。',
51
+ en: 'No sessions yet. Start one by running hncode in a terminal.',
52
+ },
53
+ 'home.live': { zh: '运行中', en: 'Running' },
54
+ 'home.saved': { zh: '已退出', en: 'Exited' },
55
+ 'home.readonly': { zh: '只读', en: 'Read-only' },
56
+ 'home.untitled': { zh: '未命名', en: 'Untitled' },
57
+ 'home.noWorkspace': { zh: '未知目录', en: 'Unknown directory' },
58
+ 'home.refresh': { zh: '刷新', en: 'Refresh' },
59
+ 'home.logout': { zh: '退出登录', en: 'Sign out' },
60
+ 'home.colTitle': { zh: '会话', en: 'Session' },
61
+ 'home.colUpdated': { zh: '最后活动', en: 'Last active' },
62
+ 'home.colRounds': { zh: '回合', en: 'Turns' },
63
+ 'home.sessionCount': { zh: '{n} 个会话', en: '{n} session(s)' },
64
+ 'home.loadFailed': { zh: '读取会话列表失败:', en: 'Could not load sessions: ' },
65
+ // Relative timestamps. They live in the table rather than as ternaries in the
66
+ // renderer so Chinese and English keep the same shape ("3 分钟前" / "3m ago"),
67
+ // and so both the landing page and the rail say it the same way.
68
+ 'home.justNow': { zh: '刚刚', en: 'just now' },
69
+ 'home.minutesAgo': { zh: '{n} 分钟前', en: '{n}m ago' },
70
+ 'home.hoursAgo': { zh: '{n} 小时前', en: '{n}h ago' },
71
+ 'home.daysAgo': { zh: '{n} 天前', en: '{n}d ago' },
72
+
73
+
74
+ // ---- navigation ----------------------------------------------------------
75
+ 'nav.chat': { zh: '对话', en: 'Chat' },
76
+ 'nav.readonly': { zh: '只读', en: 'read-only' },
77
+ 'nav.ariaLabel': { zh: '视图切换', en: 'views' },
78
+ 'rail.ariaLabel': { zh: '会话列表', en: 'sessions' },
79
+ 'nav.tasks': { zh: '任务', en: 'Tasks' },
80
+ 'nav.files': { zh: '文件', en: 'Files' },
81
+ 'nav.logs': { zh: '日志', en: 'Logs' },
82
+ 'nav.tools': { zh: '工具', en: 'Tools' },
83
+ 'nav.commands': { zh: '命令', en: 'Commands' },
84
+ 'nav.connecting': { zh: '连接中', en: 'Connecting' },
85
+ 'nav.connected': { zh: '已连接', en: 'Connected' },
86
+ 'nav.disconnected': { zh: '断开,重连中', en: 'Disconnected, retrying' },
87
+ 'nav.language': { zh: '语言', en: 'Language' },
88
+
89
+ // ---- session rail --------------------------------------------------------
90
+ 'rail.heading': { zh: '会话', en: 'Sessions' },
91
+ 'rail.empty': { zh: '还没有其它会话', en: 'No other sessions' },
92
+ 'rail.noMatch': { zh: '没有匹配的会话', en: 'No session matches' },
93
+ 'rail.noWorkspace': { zh: '未知目录', en: 'Unknown directory' },
94
+ 'rail.current': { zh: '当前', en: 'Current' },
95
+ 'rail.search': { zh: '搜索 id / 标题 / 目录', en: 'search id / title / workspace' },
96
+ 'rail.all': { zh: '所有会话', en: 'All sessions' },
97
+ 'rail.open': { zh: '会话列表', en: 'Sessions' },
98
+ // ---- chat pane -----------------------------------------------------------
99
+ 'chat.untitled': { zh: '会话', en: 'Session' },
100
+ 'chat.interrupt': { zh: '中断', en: 'Interrupt' },
101
+ 'chat.placeholder': {
102
+ zh: '输入消息,Enter 发送,Shift+Enter 换行',
103
+ en: 'Message the agent. Enter sends, Shift+Enter adds a line',
104
+ },
105
+ 'chat.send': { zh: '发送', en: 'Send' },
106
+ 'chat.shellHint': { zh: '行首加 ! 直接执行命令', en: 'A leading ! runs the line in the shell' },
107
+ 'chat.running': { zh: '运行中', en: 'Running' },
108
+ 'chat.idle': { zh: '空闲', en: 'Idle' },
109
+ 'chat.toggleHint': { zh: '点击展开原文', en: 'Click to expand' },
110
+
111
+ // ---- roles ---------------------------------------------------------------
112
+ 'role.user': { zh: '你', en: 'You' },
113
+ 'role.assistant': { zh: 'AI', en: 'AI' },
114
+ 'role.tool': { zh: '工具', en: 'Tool' },
115
+ 'role.toolResult': { zh: '输出', en: 'Output' },
116
+ 'role.system': { zh: '系统', en: 'System' },
117
+ 'role.warn': { zh: '警告', en: 'Warn' },
118
+ 'role.thinking': { zh: '思考', en: 'Thinking' },
119
+ 'role.shell': { zh: 'shell', en: 'shell' },
120
+ 'role.plan': { zh: '计划', en: 'Plan' },
121
+ 'role.compaction': { zh: '压缩', en: 'Compact' },
122
+ 'role.aborted': { zh: '中止', en: 'Aborted' },
123
+ 'role.bgTask': { zh: '任务', en: 'Task' },
124
+ 'role.error': { zh: '错误', en: 'Error' },
125
+ 'role.queued': { zh: '队列', en: 'Queued' },
126
+ 'role.steer': { zh: '注入', en: 'Steer' },
127
+
128
+ // ---- tool states ---------------------------------------------------------
129
+ 'tool.running': { zh: '运行中', en: 'running' },
130
+ 'tool.done': { zh: '完成', en: 'done' },
131
+ 'tool.failed': { zh: '失败', en: 'failed' },
132
+
133
+ // ---- compaction ----------------------------------------------------------
134
+ 'compact.running': { zh: '正在压缩', en: 'Compacting' },
135
+ 'compact.done': { zh: '压缩完成', en: 'Compaction complete' },
136
+ 'compact.cancelled': { zh: '压缩取消', en: 'Compaction cancelled' },
137
+ // `before → after`. "tokens" is a noun here, not a bare unit, so it is a word to
138
+ // translate; `tok/s` elsewhere is a symbol and is not.
139
+ 'compact.amount': { zh: '{before} → {after} tokens', en: '{before} → {after} tokens' },
140
+
141
+ // ---- tasks pane ----------------------------------------------------------
142
+ 'tasks.heading': { zh: '后台任务', en: 'Background tasks' },
143
+ 'tasks.refresh': { zh: '刷新', en: 'Refresh' },
144
+ 'tasks.empty': { zh: '没有后台任务。', en: 'No background tasks.' },
145
+ 'tasks.colId': { zh: 'ID', en: 'ID' },
146
+ 'tasks.colKind': { zh: '类型', en: 'Kind' },
147
+ 'tasks.colStatus': { zh: '状态', en: 'Status' },
148
+ 'tasks.colSummary': { zh: '摘要', en: 'Summary' },
149
+ 'tasks.stop': { zh: '停止', en: 'Stop' },
150
+
151
+ // ---- files pane ----------------------------------------------------------
152
+ 'files.heading': { zh: '会话改动', en: 'Session changes' },
153
+ 'files.sub': { zh: '本轮对话读写过的文件', en: 'Files this conversation read or wrote' },
154
+ 'files.empty': { zh: '本会话还没有读写文件。', en: 'No files read or written yet.' },
155
+ 'files.colPath': { zh: '路径', en: 'Path' },
156
+ 'files.colOps': { zh: '操作', en: 'Ops' },
157
+ 'files.colCount': { zh: '次数', en: 'Count' },
158
+
159
+ // ---- logs pane -----------------------------------------------------------
160
+ 'logs.heading': { zh: '事件日志', en: 'Event log' },
161
+ 'logs.clear': { zh: '清空', en: 'Clear' },
162
+ 'logs.sub': { zh: '最近的会话事件,最新的在最下面', en: 'Recent session events, newest last' },
163
+
164
+ // ---- tools / commands panes ----------------------------------------------
165
+ 'tools.heading': { zh: '可用工具', en: 'Tools available' },
166
+ 'tools.sub': { zh: '由本会话的模型调用', en: 'Callable by the model in this session' },
167
+ 'commands.heading': { zh: '斜杠命令', en: 'Slash commands' },
168
+ 'commands.sub': { zh: '与终端完全一致', en: 'Identical to the terminal' },
169
+
170
+ // ---- status panel --------------------------------------------------------
171
+ 'side.runtime': { zh: '运行', en: 'Runtime' },
172
+ 'settings.open': { zh: '设置', en: 'Settings' },
173
+ 'settings.title': { zh: '设置', en: 'Settings' },
174
+ 'settings.close': { zh: '关闭', en: 'Close' },
175
+ 'settings.save': { zh: '保存', en: 'Save' },
176
+ 'settings.session': { zh: '会话', en: 'Session' },
177
+ 'settings.titleLabel': { zh: '标题', en: 'Title' },
178
+ 'settings.titleSaved': { zh: '标题已保存', en: 'Title saved' },
179
+ 'settings.providers': { zh: 'Provider', en: 'Providers' },
180
+ 'settings.noProviders': { zh: '还没有配置 provider', en: 'No providers configured' },
181
+ 'settings.discover': { zh: '拉取模型', en: 'discover' },
182
+ 'settings.foundModels': { zh: '找到 {n} 个模型', en: 'Found {n} model(s)' },
183
+ 'settings.import': { zh: '导入', en: 'import' },
184
+ 'settings.imported': { zh: '已导入 {name},新增 {n} 个模型', en: 'Imported {name}, {n} new model(s)' },
185
+ 'settings.importKnown': { zh: '从 models.dev 导入', en: 'Import from models.dev' },
186
+ 'settings.keyPrompt': { zh: '请输入 {name} 的 API key:', en: 'API key for {name}:' },
187
+ 'settings.addProvider': { zh: '添加', en: 'Add' },
188
+ 'settings.added': { zh: '已添加 {name}', en: 'Added {name}' },
189
+ 'settings.remove': { zh: '删除', en: 'remove' },
190
+ 'settings.removed': { zh: '已删除 {name}', en: 'Removed {name}' },
191
+ 'settings.confirmRemove': { zh: '删除 provider {name}?它的模型也会一并移除。', en: 'Delete provider {name}? Its models go too.' },
192
+ 'settings.noMatch': { zh: '没有匹配项', en: 'No match' },
193
+ 'settings.nameRequired': { zh: '名称不能为空', en: 'Name is required' },
194
+ 'settings.failed': { zh: '操作失败:', en: 'Failed: ' },
195
+ 'settings.prefs': { zh: '偏好', en: 'Preferences' },
196
+ 'settings.info': { zh: '当前生效', en: 'In force' },
197
+ 'settings.model': { zh: '模型', en: 'Model' },
198
+ 'settings.provider': { zh: 'Provider', en: 'Provider' },
199
+ 'settings.protocol': { zh: '协议', en: 'Protocol' },
200
+ 'settings.endpoint': { zh: '端点', en: 'Endpoint' },
201
+ 'settings.key': { zh: '密钥', en: 'API key' },
202
+ 'settings.workspace': { zh: '目录', en: 'Workspace' },
203
+ 'settings.ctx': { zh: '上下文窗口', en: 'Context window' },
204
+ // Placeholders for the "add provider" row and the models.dev filter.
205
+ 'settings.phName': { zh: '名称', en: 'name' },
206
+ 'settings.phUrl': { zh: '接口地址', en: 'base URL' },
207
+ 'settings.phKey': { zh: 'API key', en: 'API key' },
208
+ 'settings.phFilter': { zh: '筛选', en: 'filter' },
209
+ // Provider row tags / sub-line fragments.
210
+ 'settings.tagKey': { zh: '已设密钥', en: 'key' },
211
+ 'settings.tagNoKey': { zh: '无密钥', en: 'no key' },
212
+ 'settings.noUrl': { zh: '未设置地址', en: '(no url)' },
213
+ 'settings.modelCount': { zh: '{n} 个模型', en: '{n} model(s)' },
214
+ 'settings.valSet': { zh: '已设置', en: 'set' },
215
+ 'settings.valNotSet': { zh: '未设置', en: 'not set' },
216
+ 'pref.calm': { zh: '简洁模式', en: 'Calm mode' },
217
+ 'pref.autoCompact': { zh: '自动压缩', en: 'Auto compact' },
218
+ 'pref.autoUpdate': { zh: '自动更新', en: 'Auto update' },
219
+ 'pref.promptCache': { zh: '提示缓存', en: 'Prompt cache' },
220
+ 'pref.external': { zh: '允许工作区外路径', en: 'Allow external paths' },
221
+
222
+ 'side.model': { zh: '模型', en: 'Model' },
223
+ 'side.provider': { zh: 'Provider', en: 'Provider' },
224
+ 'side.mode': { zh: '权限', en: 'Permission' },
225
+ 'side.cwd': { zh: '目录', en: 'Directory' },
226
+ 'side.plan': { zh: '计划模式', en: 'Plan mode' },
227
+ 'side.effort': { zh: '思考', en: 'Thinking' },
228
+ 'side.focus': { zh: '专注模式', en: 'Focus mode' },
229
+ 'side.busy': { zh: '状态', en: 'State' },
230
+ 'side.context': { zh: '上下文', en: 'Context' },
231
+ 'side.usage': { zh: '用量', en: 'Usage' },
232
+ 'side.rounds': { zh: '轮次', en: 'Rounds' },
233
+ 'side.steps': { zh: '步骤', en: 'Steps' },
234
+ 'side.rate': { zh: '速度', en: 'Rate' },
235
+ // Unit suffixes for the values above. Chinese writes the unit straight after
236
+ // the number with no space (`3轮`), English needs one (`3 turns`) — so the
237
+ // separator belongs INSIDE the string rather than being concatenated at the
238
+ // call site. `tok/s` is a unit in both languages and is left as is.
239
+ 'unit.rounds': { zh: '{n}轮', en: '{n} turns' },
240
+ 'unit.steps': { zh: '{n}步', en: '{n} steps' },
241
+ 'unit.rate': { zh: '{n} tok/s', en: '{n} tok/s' },
242
+ // The placeholder a card's field shows when it has no value at all. Distinct
243
+ // from `side.none` (「(无)」) which labels an EMPTY LIST in the rail; a field
244
+ // placeholder sits inline next to its label and is dimmed instead.
245
+ 'unit.none': { zh: '—', en: '—' },
246
+ 'side.todos': { zh: '待办', en: 'Todos' },
247
+ 'side.queue': { zh: '队列', en: 'Queue' },
248
+ 'side.quick': { zh: '快捷', en: 'Quick' },
249
+ 'side.on': { zh: '开', en: 'on' },
250
+ 'side.off': { zh: '关', en: 'off' },
251
+ 'side.none': { zh: '(无)', en: '(none)' },
252
+
253
+ // ---- queue pane ----------------------------------------------------------
254
+ // The terminal steers a queued message into the running turn with Ctrl-S; a
255
+ // browser cannot use that key, so each queued row carries its own button with
256
+ // the same effect.
257
+ // ---- copy / drawer / tool detail ------------------------------------------
258
+ // A copy button reports its own result in place: the label IS the feedback.
259
+ // ---- transcript markers ---------------------------------------------------
260
+ // The glyph the terminal prints in the gutter for each kind of row. They live
261
+ // here because the terminal and the browser must agree on them: a `❯` in the
262
+ // terminal and a `❯` in the browser are the same message.
263
+ 'mark.user': { zh: '❯', en: '❯' },
264
+ 'mark.queued': { zh: '❯', en: '❯' },
265
+ 'mark.steer': { zh: '❯', en: '❯' },
266
+ 'mark.bash': { zh: '!', en: '!' },
267
+ 'mark.warn': { zh: '⚑', en: '⚑' },
268
+ 'mark.thinking': { zh: '●', en: '●' },
269
+ 'mark.tool': { zh: '●', en: '●' },
270
+ 'mark.toolFailed': { zh: '✗', en: '✗' },
271
+ 'mark.result': { zh: '↳', en: '↳' },
272
+ 'mark.bgTask': { zh: '●', en: '●' },
273
+ 'mark.bgTaskFailed': { zh: '✗', en: '✗' },
274
+
275
+ 'tool.verb': { zh: '调用', en: 'Used' },
276
+ // The verb carries the STATE, as in the terminal: `Using …` while a call runs,
277
+ // `Used …` once it finished.
278
+ 'tool.using': { zh: '调用', en: 'Using' },
279
+ 'tool.used': { zh: '调用', en: 'Used' },
280
+ 'chat.thinking': { zh: '思考…', en: 'thinking…' },
281
+ 'tool.compacting': { zh: '正在压缩上下文…', en: 'Compacting context…' },
282
+ 'tool.compacted': { zh: '压缩完成', en: 'Compaction complete' },
283
+ 'tool.compactCancelled': { zh: '压缩取消', en: 'Compaction cancelled' },
284
+ // "show more" for a capped tool result. It is a button, so it says what
285
+ // clicking it does rather than describing a state.
286
+ 'tool.moreLines': { zh: '还有 {n} 行,展开', en: '{n} more lines, expand' },
287
+ 'tool.collapse': { zh: '收起', en: 'collapse' },
288
+ 'tool.noChanges': { zh: '没有改动', en: 'no changes' },
289
+ 'chat.shellMode': { zh: 'shell 模式,Enter 执行', en: 'shell mode, Enter runs it' },
290
+
291
+ // ---- status line ----------------------------------------------------------
292
+ // Same shape as the terminal's footer: `3 turns | 7 steps | 42 tok/s`. Kept in
293
+ // English on purpose — the terminal does not translate it either, and one
294
+ // person reading both screens should not see two different sentences.
295
+ 'status.usage': {
296
+ zh: '{turns} 轮 | {steps} 步 | {rate} tok/s',
297
+ en: '{turns} turns | {steps} steps | {rate} tok/s',
298
+ },
299
+ 'status.context': {
300
+ zh: '上下文 {pct}% ({used}/{max})',
301
+ en: 'context {pct}% ({used}/{max})',
302
+ },
303
+ 'status.tasksBash': { zh: '[{n} 个任务运行中]', en: '[{n} task(s) running]' },
304
+ 'status.tasksAgent': { zh: '[{n} 个 agent 运行中]', en: '[{n} agent(s) running]' },
305
+ 'status.expandTodos': { zh: '展开全部待办', en: 'Show every todo' },
306
+
307
+ 'copy.copy': { zh: '复制', en: 'copy' },
308
+ 'copy.copyTitle': { zh: '复制到剪贴板', en: 'Copy to clipboard' },
309
+ 'copy.done': { zh: '已复制', en: 'copied' },
310
+ 'copy.failed': { zh: '复制失败', en: 'failed' },
311
+ 'drawer.close': { zh: 'Esc 关闭', en: 'esc \u2715' },
312
+ 'tool.args': { zh: '参数', en: 'args' },
313
+
314
+ 'queue.steer': { zh: '注入', en: 'Steer' },
315
+ 'queue.steering': { zh: '注入中…', en: 'Steering…' },
316
+ 'queue.steerHint': { zh: '立即注入到正在进行的回合', en: 'Inject this into the running turn now' },
317
+ 'queue.edit': { zh: '编辑', en: 'Edit' },
318
+ 'queue.editHint': { zh: '取回输入框修改,不再排队', en: 'Pull it back into the composer to change it' },
319
+ 'queue.drop': { zh: '移除', en: 'Drop' },
320
+ 'queue.dropHint': { zh: '从队列中删除这条消息', en: 'Remove this message from the queue' },
321
+ 'queue.moreHint': { zh: '… 还有 {n} 条', en: '… +{n} more' },
322
+ 'queue.hintRunning': { zh: '等待本回合结束', en: 'Sent when this turn ends' },
323
+ 'queue.hintIdle': { zh: '回合结束后依次发送', en: 'Sent in order once the turn ends' },
324
+
325
+ // ---- popover -------------------------------------------------------------
326
+ // The status line's values are buttons; these are the menu's own strings.
327
+ 'popover.click': { zh: '点击切换', en: 'Click to change' },
328
+ 'popover.effortLabel': { zh: '思考等级', en: 'Thinking Effort' },
329
+ 'popover.effort': { zh: '思考 {v}', en: 'think {v}' },
330
+ 'popover.off': { zh: 'off', en: 'off' },
331
+ 'popover.unknown': { zh: '—', en: '—' },
332
+
333
+
334
+
335
+ // NOTE: the bottom status line and context line are deliberately NOT translated.
336
+ // tui.js prints them as fixed English (`3 turns | 7 steps | 42 tok/s | context:
337
+ // 20% (…)`) with no i18n key, and these two rows exist to be the browser's copy
338
+ // of that readout. Translating one side would make the same person read two
339
+ // different sentences for the same state.
340
+
341
+ // ---- modal ---------------------------------------------------------------
342
+ // ---- modal ---------------------------------------------------------------
343
+ 'modal.approve': { zh: '批准', en: 'Approve' },
344
+ 'modal.reject': { zh: '拒绝', en: 'Reject' },
345
+ 'modal.allow': { zh: '允许', en: 'Allow' },
346
+ 'modal.deny': { zh: '拒绝', en: 'Deny' },
347
+ 'modal.question': { zh: '需要选择', en: 'Question' },
348
+ 'modal.submit': { zh: '提交', en: 'Submit' },
349
+ 'modal.cancel': { zh: '取消', en: 'Cancel' },
350
+ 'modal.other': { zh: 'Other', en: 'Other' },
351
+ 'modal.otherHint': { zh: '自己输入答案', en: 'Type your own answer' },
352
+ 'modal.note': { zh: '补充说明', en: 'Add a note' },
353
+ 'modal.noteHint': { zh: '写给整轮问题的补充,可留空', en: 'Optional notes for the whole request' },
354
+ 'modal.next': { zh: '下一题', en: 'Next' },
355
+ 'modal.progress': { zh: '第 {n}/{total} 题', en: 'Question {n}/{total}' },
356
+
357
+ // ---- errors --------------------------------------------------------------
358
+ 'err.actionFailed': { zh: '操作失败', en: 'Action failed' },
359
+ 'err.unreachable': { zh: '连不上服务:', en: 'Cannot reach the server: ' },
360
+ 'err.unauthorized': { zh: '未授权', en: 'Unauthorized' },
361
+ // Plain-text responses from the server itself (a direct visit, a curl call) —
362
+ // these have no page to carry a client-side language choice, so the server
363
+ // negotiates from Accept-Language.
364
+ 'http.notFound': { zh: '找不到', en: 'not found' },
365
+ 'http.badMethod': { zh: '不支持该请求方法', en: 'method not allowed' },
366
+ 'http.unauthorized': { zh: '未授权', en: 'unauthorized' },
367
+ };
368
+
369
+ /**
370
+ * Look up one key. `lang` is a language id; anything unknown falls back to the
371
+ * default, then to English, then to the key itself (which makes a missing string
372
+ * obvious in situ instead of rendering as `undefined`).
373
+ */
374
+ export function t(lang, key, vars) {
375
+ const entry = MESSAGES[key];
376
+ if (!entry) return key;
377
+ let s = entry[lang] || entry[DEFAULT_LANG] || entry.en || key;
378
+ if (vars) {
379
+ for (const k of Object.keys(vars)) s = s.split('{' + k + '}').join(String(vars[k]));
380
+ }
381
+ return s;
382
+ }
383
+
384
+ /** The whole table for one language, as a flat { key: string } map. */
385
+ export function catalog(lang) {
386
+ const out = {};
387
+ for (const key of Object.keys(MESSAGES)) out[key] = t(lang, key);
388
+ return out;
389
+ }
package/src/index.js CHANGED
@@ -26,8 +26,8 @@ Options:
26
26
  -V, --version output the version number
27
27
  -S, --session [id] Resume a session (with id) or pick interactively.
28
28
  -c, --continue Continue the previous session for this directory.
29
- -y, --yolo Routine edits/commands run; risky actions still ask.
30
- --auto Never ask; everything runs automatically.
29
+ -y, --yolo Yolo mode: routine edits/commands run; risky actions still ask.
30
+ --auto Auto mode: never ask; everything runs automatically.
31
31
  -m, --model <model> Model alias to use for this invocation.
32
32
  -p, --prompt <prompt> Run one prompt non-interactively and print the reply.
33
33
  --output-format <format> Prompt mode output: text (default), json, or stream-json.
@@ -35,6 +35,9 @@ Options:
35
35
  Exit code reflects the outcome: 0 ok, 1 failure, 2 config, 3 no answer, 4 interrupted.
36
36
  --control Expose a local control socket (prompt/status/interrupt) while the TUI runs.
37
37
  --control <path> Same, at an explicit socket path.
38
+ --web Serve this session to a browser on 127.0.0.1 (random port).
39
+ --web [ip][:port] Same, on a chosen address/port (e.g. --web 0.0.0.0:8080).
40
+ --plan Start in plan mode (research only, no writes).
38
41
  --plan Start in plan mode (research only, no writes).
39
42
  --add-dir <dir> Add an additional workspace directory. Repeatable.
40
43
  -h, --help Show help.
@@ -66,7 +69,7 @@ export function parseArgs(argv) {
66
69
  if (eq >= 0) { key = a.slice(2, eq); val = a.slice(eq + 1); }
67
70
  else { key = a.slice(2); val = null; }
68
71
  if (key === 'resume') key = 'session'; // `--resume <id>` is an alias for `--session <id>`
69
- if (['session', 'control'].includes(key) && val === null) { const nx = argv[i + 1]; out[key] = nx !== undefined && !nx.startsWith('-') ? (i++, nx) : true; }
72
+ if (['session', 'control', 'web'].includes(key) && val === null) { const nx = argv[i + 1]; out[key] = nx !== undefined && !nx.startsWith('-') ? (i++, nx) : true; }
70
73
  else if (key === 'model' || key === 'prompt' || key === 'output-format' || key === 'add-dir') {
71
74
  if (val === null) {
72
75
  const nx = argv[i + 1];
@@ -323,9 +326,44 @@ function loadOrCreateSession(opts, cfg) {
323
326
  return session;
324
327
  }
325
328
 
329
+
330
+ /**
331
+ * Parse the --web flag into { bindIp, port } or null.
332
+ * --web -> loopback, OS-assigned port
333
+ * --web 8080 -> loopback, that port
334
+ * --web 0.0.0.0:8080 -> explicit address and port
335
+ * --web 192.168.1.5 -> that address, OS-assigned port
336
+ * Anything unparseable returns null, so the flag is ignored rather than failing
337
+ * the launch — /web from inside the session still works either way.
338
+ */
339
+ export function parseWebArg(v) {
340
+ if (v == null || v === false) return null;
341
+ if (v === true) return { bindIp: '127.0.0.1', port: 0 };
342
+ const s = String(v).trim();
343
+ if (!s) return { bindIp: '127.0.0.1', port: 0 };
344
+ let bindIp = '127.0.0.1';
345
+ let port = 0;
346
+ if (/^\d+$/.test(s)) {
347
+ port = Number(s);
348
+ } else if (s.includes(':')) {
349
+ // IPv6 in brackets is deliberately not handled: /web covers it, and a
350
+ // half-parsed address is worse than ignoring the flag.
351
+ const at = s.lastIndexOf(':');
352
+ bindIp = s.slice(0, at) || '127.0.0.1';
353
+ const p = s.slice(at + 1);
354
+ if (!/^\d+$/.test(p)) return null;
355
+ port = Number(p);
356
+ } else {
357
+ bindIp = s;
358
+ }
359
+ if (port < 0 || port > 65535) return null;
360
+ return { bindIp, port };
361
+ }
362
+
326
363
  export async function main(argv) {
327
364
  const args = parseArgs(argv);
328
365
  if (args.help) { printHelp(); return 0; }
366
+ if (args.help) { printHelp(); return 0; }
329
367
  if (args.version) { console.log(VERSION); return 0; }
330
368
 
331
369
  // subcommands
@@ -419,6 +457,11 @@ export async function main(argv) {
419
457
  // Remote control: expose a local socket so a script can queue a prompt or
420
458
  // query status on the running session (see ci.js).
421
459
  control: args.control === true ? '' : (typeof args.control === 'string' ? args.control : null),
460
+ // Web UI: start the server as soon as the TUI is up. `--web` alone binds
461
+ // loopback on an OS-assigned port; `--web 0.0.0.0:8080` chooses both. Parsed
462
+ // here so it behaves like every other flag, and the TUI still has /web to
463
+ // start or stop it mid-session.
464
+ web: parseWebArg(args.web),
422
465
  // MCP runs in the BACKGROUND once the TUI is up (see startTUI): connecting
423
466
  // can take seconds and must not delay the first paint.
424
467
  mcpConnect: connectMcp,