remcodex 0.1.0-beta.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/server/src/app.js +186 -0
  4. package/dist/server/src/cli.js +270 -0
  5. package/dist/server/src/controllers/codex-options.controller.js +199 -0
  6. package/dist/server/src/controllers/message.controller.js +21 -0
  7. package/dist/server/src/controllers/project.controller.js +44 -0
  8. package/dist/server/src/controllers/session.controller.js +175 -0
  9. package/dist/server/src/db/client.js +10 -0
  10. package/dist/server/src/db/migrations.js +32 -0
  11. package/dist/server/src/gateways/ws.gateway.js +60 -0
  12. package/dist/server/src/services/codex-app-server-runner.js +363 -0
  13. package/dist/server/src/services/codex-exec-runner.js +147 -0
  14. package/dist/server/src/services/codex-rollout-sync.js +977 -0
  15. package/dist/server/src/services/codex-runner.js +11 -0
  16. package/dist/server/src/services/codex-stream-events.js +478 -0
  17. package/dist/server/src/services/event-store.js +328 -0
  18. package/dist/server/src/services/project-manager.js +130 -0
  19. package/dist/server/src/services/pty-runner.js +72 -0
  20. package/dist/server/src/services/session-manager.js +1586 -0
  21. package/dist/server/src/services/session-timeline-service.js +181 -0
  22. package/dist/server/src/types/codex-launch.js +2 -0
  23. package/dist/server/src/types/models.js +37 -0
  24. package/dist/server/src/utils/ansi.js +143 -0
  25. package/dist/server/src/utils/codex-launch.js +102 -0
  26. package/dist/server/src/utils/codex-quota.js +179 -0
  27. package/dist/server/src/utils/codex-status.js +163 -0
  28. package/dist/server/src/utils/codex-ui-options.js +114 -0
  29. package/dist/server/src/utils/command.js +46 -0
  30. package/dist/server/src/utils/errors.js +16 -0
  31. package/dist/server/src/utils/ids.js +7 -0
  32. package/dist/server/src/utils/node-pty.js +29 -0
  33. package/package.json +36 -0
  34. package/scripts/fix-node-pty-helper.js +36 -0
  35. package/web/api.js +175 -0
  36. package/web/app.js +8082 -0
  37. package/web/components/composer.js +627 -0
  38. package/web/components/session-workbench.js +173 -0
  39. package/web/i18n/index.js +171 -0
  40. package/web/i18n/locales/de.js +50 -0
  41. package/web/i18n/locales/en.js +320 -0
  42. package/web/i18n/locales/es.js +50 -0
  43. package/web/i18n/locales/fr.js +50 -0
  44. package/web/i18n/locales/ja.js +50 -0
  45. package/web/i18n/locales/ko.js +50 -0
  46. package/web/i18n/locales/pt-BR.js +50 -0
  47. package/web/i18n/locales/ru.js +50 -0
  48. package/web/i18n/locales/zh-CN.js +320 -0
  49. package/web/i18n/locales/zh-Hant.js +53 -0
  50. package/web/index.html +23 -0
  51. package/web/message-rich-text.js +218 -0
  52. package/web/session-command-activity.js +980 -0
  53. package/web/session-event-adapter.js +826 -0
  54. package/web/session-timeline-reducer.js +728 -0
  55. package/web/session-timeline-renderer.js +656 -0
  56. package/web/session-ws.js +31 -0
  57. package/web/styles.css +5665 -0
  58. package/web/vendor/markdown-it.js +6969 -0
@@ -0,0 +1,320 @@
1
+ export default {
2
+ "app.name": "RemCodex",
3
+ "nav.projects": "项目",
4
+ "nav.sessions": "会话",
5
+ "marketing.headline": "手机上盯着 Codex CLI 跑任务",
6
+ "marketing.copy": "先建项目,再建会话,再把任务丢给服务端托管的 Codex CLI。",
7
+ "workspace.openSidebar": "打开会话侧边栏",
8
+ "workspace.closeSidebar": "收起会话侧边栏",
9
+ "workspace.language.select": "语言",
10
+ "workspace.language.switchToEnglish": "切换到英文",
11
+ "workspace.language.switchToChinese": "切换到中文",
12
+ "workspace.language.toggle": "EN",
13
+ "workspace.empty.eyebrow": "会话工作台",
14
+ "workspace.empty.title": "选择一个会话开始工作",
15
+ "workspace.empty.subtitle": "左侧切换已有会话,或直接新建/导入一个会话。",
16
+ "workspace.empty.newSession": "新建会话",
17
+ "workspace.empty.importCodex": "导入 Codex 会话",
18
+ "workspace.sidebar.import": "导入 Codex",
19
+ "workspace.sidebar.newSession": "+ 新建会话",
20
+ "workspace.sidebar.empty": "当前筛选条件下没有会话。",
21
+ "workspace.session.untitled": "未命名会话",
22
+ "workspace.project.untitled": "未命名项目",
23
+ "workspace.create.eyebrow": "会话",
24
+ "workspace.create.pickProjectTitle": "选择项目开始会话",
25
+ "workspace.create.close": "关闭",
26
+ "workspace.create.noProjects": "当前还没有项目。先选择一个目录,直接使用已有项目,或在该目录下创建一个新项目。",
27
+ "workspace.create.chooseDirectory": "选择目录",
28
+ "workspace.create.startSession": "开始会话",
29
+ "workspace.create.processing": "处理中...",
30
+ "workspace.create.directoryTitle": "选择目录开始会话",
31
+ "workspace.create.projectName": "新项目名称(可选)",
32
+ "workspace.create.projectNamePlaceholder": "留空则直接使用该目录,输入则在该目录下创建新项目",
33
+ "workspace.create.projectHelp": "留空会直接把当前目录当成项目目录;如果填写项目名称,就会在当前目录下新建一个项目目录再开始会话。",
34
+ "workspace.create.currentDirectory": "当前目录",
35
+ "workspace.create.pathPlaceholder": "选择目录,或直接输入目录路径",
36
+ "workspace.create.upOneLevel": "上一级",
37
+ "workspace.create.loadingDirectories": "正在加载目录...",
38
+ "workspace.create.noChildDirectories": "当前目录下没有可浏览的子目录。",
39
+ "workspace.create.backToProjects": "返回项目列表",
40
+ "workspace.create.pathRequired": "请选择目录,或直接输入目录路径。",
41
+ "workspace.import.eyebrow": "Codex",
42
+ "workspace.import.title": "导入 Codex 会话",
43
+ "workspace.import.searchPlaceholder": "搜索标题、路径或会话 ID",
44
+ "workspace.import.loading": "正在加载可导入会话...",
45
+ "workspace.import.empty": "没有匹配的 Codex 会话。",
46
+ "workspace.import.imported": "已导入",
47
+ "workspace.import.available": "可导入",
48
+ "workspace.import.syncLatest": "同步最新内容",
49
+ "workspace.import.importSession": "导入会话",
50
+ "workspace.import.syncToExisting": "将同步到现有会话 {sessionId}",
51
+ "workspace.import.importSelected": "将导入 {title}",
52
+ "workspace.import.chooseSession": "请选择一个会话",
53
+ "workspace.import.noneAvailable": "当前没有可导入的本机 Codex 会话。",
54
+ "workspace.import.invalidPrompt": "无效的导入编号。",
55
+ "workspace.import.promptHeader": "输入要导入的编号:",
56
+ "workspace.loading.session": "正在加载会话内容...",
57
+ "workspace.loading.projects": "正在加载项目列表...",
58
+ "workspace.loading.sessions": "正在加载会话列表...",
59
+ "projects.runtimeEyebrow": "运行环境",
60
+ "projects.healthTitle": "服务端健康状态",
61
+ "projects.codexCommand": "Codex 命令",
62
+ "projects.executionMode": "执行模式",
63
+ "projects.projectRoots": "项目白名单",
64
+ "projects.registryEyebrow": "项目登记",
65
+ "projects.addTitle": "新增项目",
66
+ "projects.name": "项目名",
67
+ "projects.namePlaceholder": "例如 easygo-service",
68
+ "projects.path": "本地路径",
69
+ "projects.pathPlaceholder": "/workspace/easygo-service",
70
+ "projects.register": "登记项目",
71
+ "projects.listTitle": "项目列表",
72
+ "projects.count": ({ count }) => `${count} 个项目`,
73
+ "projects.promptSessionTitle": "输入会话标题",
74
+ "projects.promptSessionDefault": "修复某个接口异常",
75
+ "sessions.filterListTitle": "筛选与列表",
76
+ "sessions.searchPlaceholder": "按标题 / 最近回复 / 命令搜索",
77
+ "sessions.showing": "显示 {visible} / {total}",
78
+ "sessions.clearFilters": "清空筛选",
79
+ "sessions.projectMeta": "项目: {value}",
80
+ "sessions.lastEventMeta": "最近事件: {value}",
81
+ "sessions.eventCount": ({ count }) => `事件 ${count}`,
82
+ "sessions.threadReady": "线程已建立",
83
+ "sessions.threadMissing": "线程未建立",
84
+ "sessions.pendingApproval": "待审批",
85
+ "sessions.emptyFiltered": "当前筛选条件下没有会话。",
86
+ "sessions.pageRange": "第 {start}-{end} 条,共 {total} 条",
87
+ "sessions.pageIndex": "第 {page} / {total} 页",
88
+ "sessions.pagePrev": "上一页",
89
+ "sessions.pageNext": "下一页",
90
+ "sessions.pendingApprovalTitle": "待审批",
91
+ "sessions.lastCommandTitle": "最近命令",
92
+ "sessions.lastReplyTitle": "最近回复",
93
+ "sessions.statusAll": ({ count }) => `全部状态 (${count})`,
94
+ "sessions.projectAll": ({ count }) => `全部项目 (${count})`,
95
+ "sessions.threadAll": "全部线程",
96
+ "sessions.threadReadyFilter": "线程已建立",
97
+ "sessions.threadMissingFilter": "线程未建立",
98
+ "sessions.sort.activity_desc": "最近活跃",
99
+ "sessions.sort.created_desc": "最新创建",
100
+ "sessions.sort.events_desc": "事件最多",
101
+ "sessions.sort.reply_desc": "最近有回复",
102
+ "session.status.idle": "空闲",
103
+ "session.status.starting": "启动中",
104
+ "session.status.running": "执行中",
105
+ "session.status.waiting_input": "等待输入",
106
+ "session.status.stopping": "停止中",
107
+ "session.status.completed": "已完成",
108
+ "session.status.failed": "失败",
109
+ "session.status.unknown": "未知状态",
110
+ "session.host.unsynced": "未同步主机",
111
+ "session.model.unsynced": "未同步模型",
112
+ "session.reasoning.unsynced": "未同步推理",
113
+ "session.elapsed": "会话 {value}",
114
+ "session.turnElapsed": "本轮 {value}",
115
+ "session.externalRunning": "外部执行中",
116
+ "session.current": "当前会话",
117
+ "session.back": "返回",
118
+ "inspect.selectionTitle": "辅助信息",
119
+ "inspect.close": "关闭",
120
+ "inspect.searchFlow": "搜索执行流",
121
+ "inspect.searchPlaceholder": "按命令、报错、回复内容搜索",
122
+ "inspect.searchPrev": "上一条",
123
+ "inspect.searchNext": "下一条",
124
+ "inspect.clearSearch": "清空",
125
+ "inspect.results": "结果",
126
+ "inspect.resultCount": ({ count }) => `${count} 条`,
127
+ "inspect.resultCountMatches": ({ count }) => `${count} 处命中`,
128
+ "inspect.emptySearch": "当前筛选条件下没有搜索结果。",
129
+ "inspect.rawEventsEmpty": "当前没有原始事件。",
130
+ "inspect.hint.permissionsDenied": "当前命令触发了系统权限拒绝,目标目录可能不在 writable workspace 内。",
131
+ "inspect.hint.readOnly": "当前运行环境包含 read-only 限制,所以写操作会直接失败。",
132
+ "inspect.hint.workspaceWrite": "当前运行环境处于 workspace-write 模式,只能写入当前可写根目录。",
133
+ "inspect.hint.noInteractiveApproval": "当前 {mode} 运行链路不支持交互审批弹窗。",
134
+ "inspect.hint.workspaceRoot": "当前 workspace root 是 {path}。",
135
+ "inspect.hint.sandboxApproval": "这次失败和当前 sandbox / approval 配置有关。",
136
+ "inspect.session": "会话",
137
+ "inspect.project": "项目",
138
+ "inspect.projectDirectory": "项目目录",
139
+ "inspect.currentCwd": "当前目录",
140
+ "inspect.executionPath": "执行链路",
141
+ "inspect.workspaceRoot": "工作区根目录",
142
+ "inspect.writableRoots": "可写根目录",
143
+ "inspect.pid": "PID",
144
+ "inspect.thread": "线程",
145
+ "inspect.runtimeHints": "限制说明",
146
+ "inspect.fetchHistory": "补拉历史",
147
+ "inspect.followBottom": "回到底部并跟随",
148
+ "inspect.autoScroll": "自动滚动:{value}",
149
+ "inspect.rawEventsDebug": "原始事件 / 调试信息",
150
+ "inspect.emptySelection": "选择一个任务后,这里会展示这一轮的输入、执行过程和最终结果。",
151
+ "inspect.detailTitle": "本轮详情",
152
+ "inspect.userInput": "用户输入",
153
+ "inspect.executionDetails": "执行详情",
154
+ "inspect.assistantReply": "Assistant 回复",
155
+ "inspect.rawStdout": "CLI 原始输出",
156
+ "inspect.output": "输出",
157
+ "inspect.viewOutputDetails": "查看输出详情",
158
+ "inspect.viewFullCommandOutput": "查看完整命令与输出",
159
+ "inspect.commandStillRunning": "命令仍在执行,输出会持续追加。",
160
+ "inspect.commandEndedWithErrors": "命令已结束,并产生错误输出。",
161
+ "inspect.commandCompletedExpand": "命令已完成,展开后可查看完整输出。",
162
+ "inspect.commandCompletedNoOutput": "命令已完成,没有额外输出。",
163
+ "inspect.executionSteps": "执行步骤",
164
+ "inspect.viewRawEvents": "查看原始事件",
165
+ "inspect.copyCommand": "复制命令",
166
+ "inspect.commandMetrics": "命令指标",
167
+ "inspect.commandOutput": "运行输出",
168
+ "inspect.copyFailed": "复制失败,请手动复制。",
169
+ "inspect.copied": "已复制",
170
+ "inspect.tool": "工具",
171
+ "inspect.userKind": "用户",
172
+ "inspect.statusKind": "状态",
173
+ "inspect.commandUnknown": "未识别命令",
174
+ "inspect.systemNotice": "系统提示",
175
+ "inspect.running": "运行中",
176
+ "inspect.error": "异常",
177
+ "inspect.warning": "Warning",
178
+ "inspect.completed": "已完成",
179
+ "inspect.stderr": "Stderr",
180
+ "inspect.duration": "耗时",
181
+ "inspect.problemCommand": "异常命令",
182
+ "inspect.longRunning": "运行较久",
183
+ "inspect.slowCommand": "慢命令",
184
+ "inspect.noOutput": "无输出",
185
+ "inspect.noOutputYetShort": "暂无输出",
186
+ "inspect.processExit": "进程退出",
187
+ "inspect.statusChange": "状态切换",
188
+ "inspect.filter.all": "全部",
189
+ "inspect.filter.assistant": "Assistant",
190
+ "inspect.filter.command": "工具 / 命令",
191
+ "inspect.filter.system": "System",
192
+ "inspect.severity.all": "全部级别",
193
+ "approval.required": "需要授权",
194
+ "approval.commandRequired": "命令执行需要授权",
195
+ "approval.fileChangeRequired": "文件修改需要授权",
196
+ "approval.extraPermissionRequired": "额外权限需要授权",
197
+ "approval.pending": "待确认",
198
+ "approval.restore": "需重新发起",
199
+ "approval.continueHint": "这一步需要你的确认后才能继续执行。",
200
+ "approval.restoreHint": "这条授权请求是从历史事件恢复的,当前运行已经结束。请重新发送这轮任务以再次发起授权。",
201
+ "approval.deny": "拒绝",
202
+ "approval.allowOnce": "允许一次",
203
+ "approval.allowForTurn": "本轮允许",
204
+ "approval.pathInWritable": "目标路径 {targetPath} 已在当前可写范围内,这次授权用于继续执行敏感操作。",
205
+ "approval.pathOutsideWorkspace": "目标路径 {targetPath} 不在当前 workspace root {workspaceRoot} 内,所以需要授权。",
206
+ "approval.pathOutsideWritable": "目标路径 {targetPath} 不在当前可写范围内,所以需要授权。",
207
+ "composer.reasoning.low": "低",
208
+ "composer.reasoning.medium": "中",
209
+ "composer.reasoning.high": "高",
210
+ "composer.reasoning.xhigh": "超高",
211
+ "composer.slashMenu": "斜杠命令",
212
+ "composer.slashLoading": "正在加载命令...",
213
+ "composer.slashEmpty": "没有匹配的命令",
214
+ "composer.quota.remaining": "剩余额度",
215
+ "composer.quota.hours": "5 小时 {percent} {remain}",
216
+ "composer.quota.week": "1 周 {percent} {reset}",
217
+ "composer.environment": "环境入口",
218
+ "composer.unsynced": "未同步",
219
+ "composer.placeholder": "描述你要处理的开发任务",
220
+ "composer.aria.message": "发消息给 Codex",
221
+ "composer.aria.model": "模型",
222
+ "composer.aria.reasoning": "强度",
223
+ "composer.aria.stop": "停止当前任务",
224
+ "composer.aria.send": "发送",
225
+ "timeline.empty": "还没有对话。",
226
+ "timeline.userMessage": "用户消息",
227
+ "timeline.assistantCommentary": "过程说明",
228
+ "timeline.assistant": "助手消息",
229
+ "timeline.thinking": "思考中...",
230
+ "timeline.command": "命令",
231
+ "timeline.commandStreaming": "命令输出持续更新中...",
232
+ "timeline.patch": "已编辑文件",
233
+ "timeline.patchStreaming": "补丁输出持续更新中...",
234
+ "timeline.activitySummary": "活动摘要",
235
+ "timeline.fileChanges": "已编辑的文件",
236
+ "timeline.file.untitled": "未命名文件",
237
+ "timeline.summary.moreItems": "等 {count} 项",
238
+ "timeline.summary.moreFiles": "等 {count} 个文件",
239
+ "timeline.summary.moreLocations": "等 {count} 个位置",
240
+ "timeline.summary.searchAt": "搜索于 {value}",
241
+ "timeline.summary.activities": "{count} 个活动",
242
+ "timeline.system": "系统",
243
+ "timeline.jumpToBottom": "回到底部",
244
+ "timeline.validation.completed": ({ count }) => (count > 1 ? `已验证 ${count} 项` : "已验证"),
245
+ "timeline.search.completed": ({ count }) => (count > 1 ? `已搜索 ${count} 项` : "已搜索"),
246
+ "timeline.browse.completed": ({ count }) => (count > 1 ? `已浏览 ${count} 个文件` : "已浏览文件"),
247
+ "timeline.edit.completed": ({ count }) => (count > 1 ? `已编辑 ${count} 个文件` : "已编辑文件"),
248
+ "timeline.executedActivities": ({ count }) => `已执行 ${count} 个活动`,
249
+ "activity.search": "搜索",
250
+ "activity.browse.single": "已查看 1 个文件",
251
+ "activity.browse.multiple": "已查看 {count} 个文件",
252
+ "activity.edit.single": "已编辑的文件",
253
+ "activity.edit.multiple": "已编辑 {count} 个文件",
254
+ "activity.validation.completed": "已验证",
255
+ "activity.running.edit": "正在编辑文件",
256
+ "activity.failed.edit": "编辑文件失败",
257
+ "activity.completed.edit": "已编辑文件",
258
+ "activity.running.search": "正在搜索",
259
+ "activity.failed.search": "搜索失败",
260
+ "activity.completed.search": "已搜索",
261
+ "activity.running.browse": "正在查看文件",
262
+ "activity.failed.browse": "查看文件失败",
263
+ "activity.completed.browse": "已浏览文件",
264
+ "activity.running.validation": "正在校验",
265
+ "activity.failed.validation": "校验失败",
266
+ "activity.completed.validation": "已验证",
267
+ "activity.running.git": "正在执行 Git 操作",
268
+ "activity.failed.git": "Git 操作失败",
269
+ "activity.completed.git": "已执行 Git 操作",
270
+ "activity.running.run": "正在执行命令",
271
+ "activity.failed.run": "命令执行失败",
272
+ "activity.completed.run": "已执行命令",
273
+ "task.commandExecuted": "已执行 {label}",
274
+ "task.commandRunning": "正在执行 {label}",
275
+ "task.commandFailed": "执行失败 {label}",
276
+ "task.processing": "正在处理",
277
+ "task.empty": "暂无可展示的执行过程。",
278
+ "command.outputCount": ({ count }) => `输出 ${count} 条`,
279
+ "command.stdoutCount": ({ count }) => `stdout ${count} 条`,
280
+ "command.stderrCount": ({ count }) => `stderr ${count} 条`,
281
+ "command.elapsedLabel": "耗时 {value}",
282
+ "command.runningForLabel": "已运行 {value}",
283
+ "command.summary.running": "命令执行中...",
284
+ "command.summary.completed": "命令已完成。",
285
+ "command.summary.failedExpand": "命令执行失败,展开可查看完整输出。",
286
+ "command.summary.completedWithStderr": "命令已完成,但包含错误输出。",
287
+ "command.summary.completedExpand": "命令已完成,展开可查看完整输出。",
288
+ "runtime.low": "低",
289
+ "runtime.medium": "中",
290
+ "runtime.high": "高",
291
+ "runtime.xhigh": "超高",
292
+ "generic.close": "关闭",
293
+ "generic.back": "返回",
294
+ "generic.refresh": "刷新",
295
+ "generic.online": "在线",
296
+ "generic.search": "搜索",
297
+ "generic.project": "项目",
298
+ "generic.status": "状态",
299
+ "generic.sort": "排序",
300
+ "generic.keyword": "关键词",
301
+ "generic.clear": "清空",
302
+ "generic.copy": "复制",
303
+ "generic.expand": "展开",
304
+ "generic.collapse": "收起",
305
+ "generic.type": "类型",
306
+ "generic.level": "级别",
307
+ "generic.on": "开",
308
+ "generic.off": "关",
309
+ "generic.notSynced": "未同步",
310
+ "generic.notStarted": "未启动",
311
+ "generic.notEstablished": "未建立",
312
+ "generic.showing": "显示 {visible} / {total}",
313
+ "generic.segmentCount": ({ count }) => `${count} 段`,
314
+ "generic.noExtraOutput": "无额外输出。",
315
+ "generic.noOutputYet": "这条命令还没有输出。",
316
+ "generic.unknown": "未知",
317
+ "generic.none": "暂无",
318
+ "composer.slashUnavailable": "当前命令暂时不可用。",
319
+ "composer.slashExecuted": "{slash} 已执行。",
320
+ };
@@ -0,0 +1,53 @@
1
+ import en from "./en.js";
2
+
3
+ export default {
4
+ ...en,
5
+ "nav.projects": "專案",
6
+ "nav.sessions": "會話",
7
+ "workspace.openSidebar": "打開會話側欄",
8
+ "workspace.closeSidebar": "收起會話側欄",
9
+ "workspace.empty.eyebrow": "會話工作區",
10
+ "workspace.empty.title": "選擇一個會話開始工作",
11
+ "workspace.empty.subtitle": "從側欄切換會話,或直接新建 / 匯入一個會話。",
12
+ "workspace.empty.newSession": "新建會話",
13
+ "workspace.empty.importCodex": "匯入 Codex 會話",
14
+ "workspace.sidebar.import": "匯入 Codex",
15
+ "workspace.sidebar.newSession": "+ 新建會話",
16
+ "workspace.sidebar.empty": "目前篩選條件下沒有會話。",
17
+ "workspace.session.untitled": "未命名會話",
18
+ "workspace.loading.session": "正在載入會話內容...",
19
+ "workspace.loading.projects": "正在載入專案列表...",
20
+ "workspace.loading.sessions": "正在載入會話列表...",
21
+ "session.status.idle": "空閒",
22
+ "session.status.starting": "啟動中",
23
+ "session.status.running": "執行中",
24
+ "session.status.waiting_input": "等待輸入",
25
+ "session.status.stopping": "停止中",
26
+ "session.status.completed": "已完成",
27
+ "session.status.failed": "失敗",
28
+ "session.current": "目前會話",
29
+ "approval.required": "需要授權",
30
+ "approval.pending": "待確認",
31
+ "approval.restore": "需重新發起",
32
+ "approval.deny": "拒絕",
33
+ "approval.allowOnce": "允許一次",
34
+ "approval.allowForTurn": "本輪允許",
35
+ "composer.placeholder": "描述你要處理的開發任務",
36
+ "timeline.empty": "還沒有對話。",
37
+ "timeline.thinking": "思考中...",
38
+ "generic.close": "關閉",
39
+ "generic.back": "返回",
40
+ "generic.refresh": "重新整理",
41
+ "generic.search": "搜尋",
42
+ "generic.project": "專案",
43
+ "generic.status": "狀態",
44
+ "generic.sort": "排序",
45
+ "generic.keyword": "關鍵詞",
46
+ "generic.copy": "複製",
47
+ "generic.expand": "展開",
48
+ "generic.collapse": "收起",
49
+ "generic.on": "開",
50
+ "generic.off": "關",
51
+ "generic.none": "暫無",
52
+ "inspect.selectionTitle": "輔助資訊",
53
+ };
package/web/index.html ADDED
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
8
+ />
9
+ <title>RemCodex</title>
10
+ <link rel="stylesheet" href="/styles.css" />
11
+ </head>
12
+ <body>
13
+ <div class="background-glow background-glow-left"></div>
14
+ <div class="background-glow background-glow-right"></div>
15
+
16
+ <main class="app-shell">
17
+ <section id="app" class="page page-root"></section>
18
+ </main>
19
+
20
+ <script src="/vendor/markdown-it.js"></script>
21
+ <script type="module" src="/app.js"></script>
22
+ </body>
23
+ </html>
@@ -0,0 +1,218 @@
1
+ export function escapeHtml(value) {
2
+ return String(value ?? "")
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#39;");
8
+ }
9
+
10
+ function prepareMarkdownSource(text, options = {}) {
11
+ const raw = String(text ?? "");
12
+ if (!raw.trim()) {
13
+ return "";
14
+ }
15
+
16
+ let prepared = raw.replace(/\r\n?/g, "\n");
17
+
18
+ // 1) 让常见块级结构尽量提前成型
19
+ // 普通文本后面直接接列表 / 引用 / 代码块 / 标题时,补一个空行
20
+ prepared = prepared.replace(
21
+ /([^\n])\n(?=(?:\s{0,3}(?:[-*+]|\d+\.)\s+|\s{0,3}>|\s{0,3}```|\s{0,3}#{1,6}\s))/g,
22
+ "$1\n\n",
23
+ );
24
+
25
+ if (!options.streaming) {
26
+ return prepared;
27
+ }
28
+
29
+ // 2) streaming 时,补全未闭合 fence
30
+ const fenceMatches = prepared.match(/^\s*```.*$/gm) || [];
31
+ if (fenceMatches.length % 2 === 1) {
32
+ prepared += "\n```";
33
+ }
34
+
35
+ // 3) streaming 时,补全未闭合行内 code
36
+ const backtickCount = (prepared.match(/`/g) || []).length;
37
+ if (backtickCount % 2 === 1) {
38
+ prepared += "`";
39
+ }
40
+
41
+ // 4) streaming 时,补全未闭合粗体 **
42
+ // 只做最常见场景,避免过度魔改
43
+ const doubleStarCount = (prepared.match(/\*\*/g) || []).length;
44
+ if (doubleStarCount % 2 === 1) {
45
+ prepared += "**";
46
+ }
47
+
48
+ // 5) streaming 时,补全未闭合单星号 *
49
+ // 这里做保守处理:排除已经被 ** 吃掉的情况
50
+ const singleStarLike = prepared.replace(/\*\*/g, "");
51
+ const singleStarCount = (singleStarLike.match(/\*/g) || []).length;
52
+ if (singleStarCount % 2 === 1) {
53
+ prepared += "*";
54
+ }
55
+
56
+ // 6) 末尾如果停在 blockquote 行内,保留块语义更稳定
57
+ // (这里不强补 >,只确保 blockquote 前已经断段)
58
+ prepared = prepared.replace(/([^\n])\n(?=\s{0,3}>\s?)/g, "$1\n\n");
59
+
60
+ return prepared;
61
+ }
62
+
63
+ function renderPlainText(text, options) {
64
+ const value = String(text || "");
65
+ if (!value) {
66
+ return "";
67
+ }
68
+ if (typeof options.renderText === "function") {
69
+ return options.renderText(value);
70
+ }
71
+ return escapeHtml(value);
72
+ }
73
+
74
+ function renderCodeText(text, options) {
75
+ const value = String(text || "");
76
+ if (!value) {
77
+ return "";
78
+ }
79
+ if (typeof options.renderCodeText === "function") {
80
+ return options.renderCodeText(value);
81
+ }
82
+ return renderPlainText(value, options);
83
+ }
84
+
85
+ function sanitizeHref(href) {
86
+ const value = String(href || "").trim();
87
+ if (!value) {
88
+ return null;
89
+ }
90
+
91
+ if (
92
+ value.startsWith("/") ||
93
+ value.startsWith("./") ||
94
+ value.startsWith("../") ||
95
+ value.startsWith("#") ||
96
+ /^https?:\/\//i.test(value)
97
+ ) {
98
+ return value;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ function getMarkdownItFactory() {
105
+ const factory = globalThis.markdownit;
106
+ if (typeof factory !== "function") {
107
+ throw new Error("markdown-it is not loaded.");
108
+ }
109
+ return factory;
110
+ }
111
+
112
+ function renderDefaultCodeBlock(block, options) {
113
+ const lang = String(block.lang || "").trim() || "text";
114
+ const code = String(block.text || "").replace(/\n$/, "");
115
+ return `<div class="msg-md-code-block"><pre class="msg-md-pre"><code class="msg-md-code">${renderCodeText(code, options)}</code></pre></div>`;
116
+ }
117
+
118
+ function createMarkdownRenderer(options) {
119
+ const MarkdownIt = getMarkdownItFactory();
120
+ const md = MarkdownIt({
121
+ html: false,
122
+ linkify: true,
123
+ breaks: true,
124
+ });
125
+
126
+ md.validateLink = (url) => Boolean(sanitizeHref(url));
127
+
128
+ const linkStack = [];
129
+
130
+ md.renderer.rules.paragraph_open = () => '<p class="msg-md-p">';
131
+ md.renderer.rules.paragraph_close = () => "</p>";
132
+ md.renderer.rules.bullet_list_open = () => '<ul class="msg-md-ul">';
133
+ md.renderer.rules.bullet_list_close = () => "</ul>";
134
+ md.renderer.rules.ordered_list_open = () => '<ol class="msg-md-ol">';
135
+ md.renderer.rules.ordered_list_close = () => "</ol>";
136
+ md.renderer.rules.list_item_open = () => '<li class="msg-md-li">';
137
+ md.renderer.rules.list_item_close = () => "</li>";
138
+ md.renderer.rules.blockquote_open = () => '<blockquote class="msg-md-bq">';
139
+ md.renderer.rules.blockquote_close = () => "</blockquote>";
140
+ md.renderer.rules.softbreak = () => "<br>";
141
+ md.renderer.rules.hardbreak = () => "<br>";
142
+ md.renderer.rules.text = (tokens, idx) => renderPlainText(tokens[idx].content || "", options);
143
+ md.renderer.rules.code_inline = (tokens, idx) =>
144
+ `<code class="msg-md-code-inline">${renderCodeText(tokens[idx].content || "", options)}</code>`;
145
+
146
+ md.renderer.rules.fence = (tokens, idx) => {
147
+ const token = tokens[idx];
148
+ const block = {
149
+ lang: String(token.info || "").trim(),
150
+ text: token.content || "",
151
+ };
152
+ if (typeof options.renderCodeBlock === "function") {
153
+ return options.renderCodeBlock(block, {
154
+ escapeHtml,
155
+ renderCodeText: (value) => renderCodeText(value, options),
156
+ });
157
+ }
158
+ return renderDefaultCodeBlock(block, options);
159
+ };
160
+
161
+ md.renderer.rules.code_block = (tokens, idx) => {
162
+ const token = tokens[idx];
163
+ const block = {
164
+ lang: "",
165
+ text: token.content || "",
166
+ };
167
+ if (typeof options.renderCodeBlock === "function") {
168
+ return options.renderCodeBlock(block, {
169
+ escapeHtml,
170
+ renderCodeText: (value) => renderCodeText(value, options),
171
+ });
172
+ }
173
+ return renderDefaultCodeBlock(block, options);
174
+ };
175
+
176
+ md.renderer.rules.link_open = (tokens, idx) => {
177
+ const href = sanitizeHref(tokens[idx].attrGet("href"));
178
+ if (!href) {
179
+ linkStack.push("span");
180
+ return '<span class="msg-md-a">';
181
+ }
182
+
183
+ linkStack.push("a");
184
+ return `<a class="msg-md-a" href="${escapeHtml(href)}" target="_blank" rel="noreferrer noopener">`;
185
+ };
186
+
187
+ md.renderer.rules.link_close = () => {
188
+ const tag = linkStack.pop() || "a";
189
+ return tag === "span" ? "</span>" : "</a>";
190
+ };
191
+
192
+ md.renderer.rules.image = (tokens, idx) => {
193
+ const token = tokens[idx];
194
+ const alt = token.content || token.attrGet("alt") || "";
195
+ return renderPlainText(alt, options);
196
+ };
197
+
198
+ return md;
199
+ }
200
+
201
+ let defaultRenderer = null;
202
+
203
+ function getDefaultRenderer() {
204
+ if (!defaultRenderer) {
205
+ defaultRenderer = createMarkdownRenderer({});
206
+ }
207
+ return defaultRenderer;
208
+ }
209
+
210
+ export function renderRichText(text, options = {}) {
211
+ const source = prepareMarkdownSource(text, options);
212
+ if (!source) return "";
213
+
214
+ const renderer =
215
+ Object.keys(options).length === 0 ? getDefaultRenderer() : createMarkdownRenderer(options);
216
+
217
+ return renderer.render(source);
218
+ }