@happy-nut/monacori 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/i18n.js ADDED
@@ -0,0 +1,266 @@
1
+ // UI message catalog for the live English / Korean switch.
2
+ //
3
+ // The viewer ships both languages and switches client-side with NO reload: every translatable
4
+ // server-rendered element in render.ts carries data-i18n (textContent), data-i18n-ph (placeholder),
5
+ // data-i18n-title (title) or data-i18n-aria (aria-label); applyI18n() in viewer.client.js rewrites
6
+ // them, and t(key) feeds the dynamically-built UI. English is the first-paint default.
7
+ //
8
+ // Keys are stable + dot-namespaced. Excluded by design (NOT translated): diff/code content, file
9
+ // paths, syntax-language names, the "monacori" brand, version strings, and literal <kbd> key names
10
+ // (F7, Cmd/Ctrl+B, …). Korean is written for Korean developers — natural, with common technical
11
+ // terms left readable (커밋, 탭, 인덱스 …) rather than force-translated.
12
+ export const MESSAGES = {
13
+ en: {
14
+ // Tabs (sidebar)
15
+ "tab.changes": "Changes",
16
+ "tab.files": "Files",
17
+ // Sidebar footer / About
18
+ "sidebar.updateAvailable": "update available",
19
+ "about.title": "About monacori",
20
+ "terminal.title": "Terminal",
21
+ "terminal.toggle": "Toggle terminal (Ctrl+`)",
22
+ "terminal.close": "Close terminal",
23
+ // Review status (toolbar) — units; the numeric count stays dynamic and is prepended at runtime.
24
+ "status.files": "files",
25
+ "status.hunks": "hunks",
26
+ "status.wsIgnored": "ws ignored",
27
+ "status.wsIgnored.title": "Whitespace ignored — Cmd/Ctrl+Shift+W",
28
+ "status.indexed": "indexed",
29
+ "status.index.title": "Go-to-definition index",
30
+ "status.indexing": "indexing",
31
+ "status.watching": "watching",
32
+ "status.live.updated": "Live: updated",
33
+ "status.live.waiting": "Live: waiting for diff server",
34
+ // Diff view
35
+ "btn.viewed": "Viewed",
36
+ "btn.viewed.title": "Toggle viewed (<)",
37
+ "diff.noDiff": "No diff to review.",
38
+ // Source toolbar
39
+ "source.title": "Source",
40
+ "source.selectFile": "Select a file from the Files tab.",
41
+ "http.env.title": "HTTP Client environment",
42
+ "http.env.aria": "HTTP environment",
43
+ "btn.diff": "Diff",
44
+ "source.loading": "Loading source…",
45
+ "source.previewUnavailable": "Source preview unavailable.",
46
+ "source.viewRaw": "Raw",
47
+ "source.viewRendered": "Rendered",
48
+ "source.buildingTree": "Building file tree…",
49
+ // Quick open
50
+ "quickopen.aria": "Quick open",
51
+ "quickopen.searchFiles": "Search files",
52
+ "quickopen.recent": "Recent files",
53
+ "quickopen.findInFiles": "Find in Files",
54
+ "quickopen.noFiles": "No files found.",
55
+ // Usages
56
+ "usages.aria": "Usages",
57
+ "usages.title": "Usages",
58
+ // Settings — nav
59
+ "settings.aria": "Settings",
60
+ "settings.title": "Settings",
61
+ "settings.cat.general": "General",
62
+ "settings.cat.prompts": "Merge prompts",
63
+ // Settings — General
64
+ "settings.language": "Language",
65
+ "settings.checkingUpdates": "Checking for updates…",
66
+ "settings.updateRestart": "Update & Restart",
67
+ "settings.upToDate": "Up to date",
68
+ "settings.updateAvailable": "Update available",
69
+ "settings.updating": "Updating… installing latest, the app will restart",
70
+ "settings.updated": "Updated. Restarting…",
71
+ "settings.updateFailed": "Update failed — try again, or run: npm i -g @happy-nut/monacori",
72
+ "settings.kbd.title": "Keyboard shortcuts",
73
+ "settings.kbd.cat.nav": "Navigation",
74
+ "settings.kbd.cat.review": "Review",
75
+ "settings.kbd.cat.terminal": "Terminal",
76
+ // Settings — keyboard-shortcut labels (descriptions only; <kbd> key names stay literal)
77
+ "kbd.nextChange": "Next change",
78
+ "kbd.prevChange": "Previous change",
79
+ "kbd.closeTab": "Close tab",
80
+ "kbd.prevNextTab": "Prev / next tab",
81
+ "kbd.cursorBackForward": "Cursor back / forward",
82
+ "kbd.findFile": "Find file",
83
+ "kbd.findInFiles": "Find in files",
84
+ "kbd.recentFiles": "Recent files",
85
+ "kbd.defUsages": "Definition / usages",
86
+ "kbd.goToDef": "Go to definition",
87
+ "kbd.filesChangesTab": "Files / Changes tab",
88
+ "kbd.sidebarContent": "Sidebar ↔ content",
89
+ "kbd.wordJump": "Word jump (vim w)",
90
+ "kbd.lineStartEnd": "Line start / end",
91
+ "kbd.extendSelection": "Extend selection",
92
+ "kbd.toggleViewed": "Toggle viewed",
93
+ "kbd.addQuestionChange": "Add question / change",
94
+ "kbd.allQuestionsChanges": "All questions / changes",
95
+ "kbd.ignoreWhitespace": "Ignore whitespace",
96
+ "kbd.saveComment": "Save comment",
97
+ "kbd.promptMemo": "Prompt memo",
98
+ "kbd.toggleTerminal": "Toggle terminal",
99
+ "kbd.splitPane": "Split pane",
100
+ "kbd.focusPane": "Focus prev / next pane",
101
+ "kbd.renamePane": "Rename pane",
102
+ "kbd.closeTerminal": "Close terminal (when focused)",
103
+ // Settings — Merge prompts
104
+ "mergePrompts.title": "Merge prompts",
105
+ "mergePrompts.desc": "Heading prepended to the merged prompt opened with Cmd/Ctrl+Shift+/ (questions) and Cmd/Ctrl+Shift+. (change requests). Leave blank to use the default.",
106
+ "mergePrompts.qHeading": "Questions heading",
107
+ "mergePrompts.cHeading": "Change-requests heading",
108
+ "mergePrompts.reset": "Reset to defaults",
109
+ "settings.saved": "Saved",
110
+ // Composer (per-line question / change-request)
111
+ "composer.question": "Ask a question about this line",
112
+ "composer.changeRequest": "Request a change for this line",
113
+ "composer.save": "Comment",
114
+ "composer.cancel": "Cancel",
115
+ "composer.hint": "Cmd/Ctrl+Enter to save, Esc to cancel",
116
+ "composer.delete": "Delete",
117
+ "comment.kind.q": "❓ Question",
118
+ "comment.kind.c": "✎ Change request",
119
+ "badge.questions": "question(s)",
120
+ "badge.changeRequests": "change request(s)",
121
+ // Merged comments modal
122
+ "merged.qTitle": "Question comments",
123
+ "merged.cTitle": "Change-request comments",
124
+ "merged.copyAll": "Copy all",
125
+ "merged.sendToTerminal": "Send to terminal",
126
+ "merged.copied": "Copied",
127
+ "merged.copyFailed": "Copy failed",
128
+ "merged.close": "Close",
129
+ "merged.qHeading": "# Questions",
130
+ "merged.cHeading": "# Change requests",
131
+ // Prompt memo (Cmd/Ctrl+Shift+N) — a single freeform Markdown scratchpad with a live split preview.
132
+ "memo.title": "Prompt memo",
133
+ "memo.placeholder": "Jot down what you're planning, in Markdown…",
134
+ "memo.previewEmpty": "Markdown preview shows up here as you type.",
135
+ // Merge-prompt default agent contracts (these follow the locale — a Korean user gets Korean defaults)
136
+ "mergePrompt.default.q": "The following are questions about code you just wrote. Answer each one — explain the intent, rationale, or context. Do not change any code; this clarifies understanding before any revisions.",
137
+ "mergePrompt.default.c": "The following are change requests for code you just wrote. For each, edit the code at the quoted location to satisfy the request. Keep changes minimal and focused; do not make unrelated edits.",
138
+ },
139
+ ko: {
140
+ // Tabs (sidebar)
141
+ "tab.changes": "변경사항",
142
+ "tab.files": "파일",
143
+ // Sidebar footer / About
144
+ "sidebar.updateAvailable": "업데이트 있음",
145
+ "about.title": "monacori 정보",
146
+ "terminal.title": "터미널",
147
+ "terminal.toggle": "터미널 토글 (Ctrl+`)",
148
+ "terminal.close": "터미널 닫기",
149
+ // Review status (toolbar)
150
+ "status.files": "개 파일",
151
+ "status.hunks": "개 변경 묶음",
152
+ "status.wsIgnored": "공백 무시",
153
+ "status.wsIgnored.title": "공백 무시 — Cmd/Ctrl+Shift+W",
154
+ "status.indexed": "개 인덱싱됨",
155
+ "status.index.title": "정의로 이동 인덱스",
156
+ "status.indexing": "인덱싱 중",
157
+ "status.watching": "감시 중",
158
+ "status.live.updated": "실시간: 업데이트됨",
159
+ "status.live.waiting": "실시간: diff 서버 대기 중",
160
+ // Diff view
161
+ "btn.viewed": "확인함",
162
+ "btn.viewed.title": "확인 표시 토글 (<)",
163
+ "diff.noDiff": "검토할 변경사항이 없습니다.",
164
+ // Source toolbar
165
+ "source.title": "소스",
166
+ "source.selectFile": "파일 탭에서 파일을 선택하세요.",
167
+ "http.env.title": "HTTP 클라이언트 환경",
168
+ "http.env.aria": "HTTP 환경",
169
+ "btn.diff": "Diff",
170
+ "source.loading": "소스 불러오는 중…",
171
+ "source.previewUnavailable": "소스 미리보기를 사용할 수 없습니다.",
172
+ "source.viewRaw": "원문",
173
+ "source.viewRendered": "렌더링",
174
+ "source.buildingTree": "파일 트리 만드는 중…",
175
+ // Quick open
176
+ "quickopen.aria": "빠른 열기",
177
+ "quickopen.searchFiles": "파일 검색",
178
+ "quickopen.recent": "최근 파일",
179
+ "quickopen.findInFiles": "파일 내용 검색",
180
+ "quickopen.noFiles": "파일을 찾을 수 없습니다.",
181
+ // Usages
182
+ "usages.aria": "사용처",
183
+ "usages.title": "사용처",
184
+ // Settings — nav
185
+ "settings.aria": "설정",
186
+ "settings.title": "설정",
187
+ "settings.cat.general": "일반",
188
+ "settings.cat.prompts": "병합 프롬프트",
189
+ // Settings — General
190
+ "settings.language": "언어",
191
+ "settings.checkingUpdates": "업데이트 확인 중…",
192
+ "settings.updateRestart": "업데이트 후 재시작",
193
+ "settings.upToDate": "최신 버전입니다",
194
+ "settings.updateAvailable": "업데이트 있음",
195
+ "settings.updating": "업데이트 중… 최신 버전을 설치하면 앱이 재시작됩니다",
196
+ "settings.updated": "업데이트 완료. 재시작 중…",
197
+ "settings.updateFailed": "업데이트 실패 — 다시 시도하거나 실행하세요: npm i -g @happy-nut/monacori",
198
+ "settings.kbd.title": "키보드 단축키",
199
+ "settings.kbd.cat.nav": "탐색",
200
+ "settings.kbd.cat.review": "리뷰",
201
+ "settings.kbd.cat.terminal": "터미널",
202
+ // Settings — keyboard-shortcut labels
203
+ "kbd.nextChange": "다음 변경",
204
+ "kbd.prevChange": "이전 변경",
205
+ "kbd.closeTab": "탭 닫기",
206
+ "kbd.prevNextTab": "이전 / 다음 탭",
207
+ "kbd.cursorBackForward": "커서 뒤로 / 앞으로",
208
+ "kbd.findFile": "파일 찾기",
209
+ "kbd.findInFiles": "파일 내용 찾기",
210
+ "kbd.recentFiles": "최근 파일",
211
+ "kbd.defUsages": "정의 / 사용처",
212
+ "kbd.goToDef": "정의로 이동",
213
+ "kbd.filesChangesTab": "파일 / 변경사항 탭",
214
+ "kbd.sidebarContent": "사이드바 ↔ 본문",
215
+ "kbd.wordJump": "단어 단위 이동 (vim w)",
216
+ "kbd.lineStartEnd": "줄 시작 / 끝",
217
+ "kbd.extendSelection": "선택 영역 확장",
218
+ "kbd.toggleViewed": "확인 표시 토글",
219
+ "kbd.addQuestionChange": "질문 / 변경요청 추가",
220
+ "kbd.allQuestionsChanges": "전체 질문 / 변경요청",
221
+ "kbd.ignoreWhitespace": "공백 무시",
222
+ "kbd.saveComment": "코멘트 저장",
223
+ "kbd.promptMemo": "프롬프트 메모",
224
+ "kbd.toggleTerminal": "터미널 토글",
225
+ "kbd.splitPane": "패널 분할",
226
+ "kbd.focusPane": "이전 / 다음 패널로 이동",
227
+ "kbd.renamePane": "패널 이름 변경",
228
+ "kbd.closeTerminal": "터미널 닫기 (포커스 시)",
229
+ // Settings — Merge prompts
230
+ "mergePrompts.title": "병합 프롬프트",
231
+ "mergePrompts.desc": "Cmd/Ctrl+Shift+/ (질문) 및 Cmd/Ctrl+Shift+. (변경요청)로 여는 병합 프롬프트 맨 앞에 붙는 머리말입니다. 비워 두면 기본값을 사용합니다.",
232
+ "mergePrompts.qHeading": "질문 머리말",
233
+ "mergePrompts.cHeading": "변경요청 머리말",
234
+ "mergePrompts.reset": "기본값으로 초기화",
235
+ "settings.saved": "저장됨",
236
+ // Composer
237
+ "composer.question": "이 줄에 대해 질문하기",
238
+ "composer.changeRequest": "이 줄에 대한 변경 요청하기",
239
+ "composer.save": "코멘트",
240
+ "composer.cancel": "취소",
241
+ "composer.hint": "Cmd/Ctrl+Enter로 저장, Esc로 취소",
242
+ "composer.delete": "삭제",
243
+ "comment.kind.q": "❓ 질문",
244
+ "comment.kind.c": "✎ 변경 요청",
245
+ "badge.questions": "개 질문",
246
+ "badge.changeRequests": "개 변경 요청",
247
+ // Merged comments modal
248
+ "merged.qTitle": "질문 코멘트",
249
+ "merged.cTitle": "변경 요청 코멘트",
250
+ "merged.copyAll": "전체 복사",
251
+ "merged.sendToTerminal": "터미널로 전송",
252
+ "merged.copied": "복사됨",
253
+ "merged.copyFailed": "복사 실패",
254
+ "merged.close": "닫기",
255
+ // Structural markers stay English in both locales (the preamble prose below follows the locale).
256
+ "merged.qHeading": "# Questions",
257
+ "merged.cHeading": "# Change requests",
258
+ // 프롬프트 메모 (Cmd/Ctrl+Shift+N) — 라이브 분할 미리보기가 있는 자유 형식 마크다운 메모 한 장.
259
+ "memo.title": "프롬프트 메모",
260
+ "memo.placeholder": "구상 중인 것을 마크다운으로 적어 보세요…",
261
+ "memo.previewEmpty": "입력하면 여기에 마크다운 미리보기가 나타납니다.",
262
+ // Merge-prompt default agent contracts (Korean default for Korean users)
263
+ "mergePrompt.default.q": "다음은 방금 작성한 코드에 대한 질문입니다. 각 질문에 답하면서 의도, 근거, 맥락을 설명하세요. 코드는 변경하지 마세요. 이 단계는 수정에 앞서 이해를 명확히 하기 위한 것입니다.",
264
+ "mergePrompt.default.c": "다음은 방금 작성한 코드에 대한 변경 요청입니다. 각 요청에 대해 인용된 위치의 코드를 수정하여 요구사항을 충족하세요. 변경은 최소한으로 집중해서 하고, 관련 없는 수정은 하지 마세요.",
265
+ },
266
+ };
package/dist/preload.cjs CHANGED
@@ -13,6 +13,33 @@ electron_1.contextBridge.exposeInMainWorld("monacoriMenu", {
13
13
  onMergedView: (cb) => {
14
14
  electron_1.ipcRenderer.on("monacori:merged-view", (_event, kind) => cb(kind));
15
15
  },
16
+ // Review menu's Cmd/Ctrl+Shift+N -> open/close the prompt memo in the renderer.
17
+ onOpenMemo: (cb) => {
18
+ electron_1.ipcRenderer.on("monacori:open-memo", () => cb());
19
+ },
20
+ // Electron watch: main pushes the rebuilt review HTML so the renderer refreshes the diff in place
21
+ // (no window reload), keeping the integrated terminal's pty sessions alive.
22
+ onDiffUpdate: (cb) => {
23
+ electron_1.ipcRenderer.on("monacori:diff-update", (_event, html) => cb(html));
24
+ },
25
+ // Cmd/Ctrl+W from the Window menu -> close the active Files-mode tab in the renderer.
26
+ onCloseTab: (cb) => {
27
+ electron_1.ipcRenderer.on("monacori:close-tab", () => cb());
28
+ },
29
+ // Terminal menu accelerators (Ctrl+`/Alt+F12 toggle, Cmd+D split) — routed via the menu because
30
+ // Chromium swallows Cmd+D before it reaches the renderer's keydown handler.
31
+ onTerminalToggle: (cb) => {
32
+ electron_1.ipcRenderer.on("monacori:terminal-toggle", () => cb());
33
+ },
34
+ onTerminalSplit: (cb) => {
35
+ electron_1.ipcRenderer.on("monacori:terminal-split", () => cb());
36
+ },
37
+ onTerminalPaneFocus: (cb) => {
38
+ electron_1.ipcRenderer.on("monacori:terminal-pane-focus", (_event, delta) => cb(delta));
39
+ },
40
+ onTerminalPaneRename: (cb) => {
41
+ electron_1.ipcRenderer.on("monacori:terminal-pane-rename", () => cb());
42
+ },
16
43
  });
17
44
  // Phase 2 lazy-LOAD: fetch a single file's diff body from the main process on demand, so the initial
18
45
  // HTML can omit the embedded diff bodies (tens of MB on big repos) and stay small.
@@ -20,3 +47,46 @@ electron_1.contextBridge.exposeInMainWorld("monacoriFile", {
20
47
  get: (index, kind) => electron_1.ipcRenderer.invoke("monacori:get-file", { index, kind }),
21
48
  getSourceData: () => electron_1.ipcRenderer.invoke("monacori:get-source-data"),
22
49
  });
50
+ // Self-update: ask the main process to install the latest version globally and relaunch. Only present
51
+ // in the Electron app (not browser/watch mode), so the renderer hides the in-app update button there.
52
+ electron_1.contextBridge.exposeInMainWorld("monacoriUpdate", {
53
+ run: () => electron_1.ipcRenderer.invoke("monacori:self-update"),
54
+ });
55
+ // Integrated terminal: bridge the renderer's xterm view to a node-pty owned by the main process (the
56
+ // sandboxed renderer can't spawn a pty). Only present in the Electron app; browser/serve mode lacks it,
57
+ // so the renderer keeps the terminal panel hidden there.
58
+ electron_1.contextBridge.exposeInMainWorld("monacoriPty", {
59
+ spawn: (size) => electron_1.ipcRenderer.invoke("monacori:pty-spawn", size),
60
+ write: (msg) => electron_1.ipcRenderer.send("monacori:pty-write", msg),
61
+ resize: (msg) => electron_1.ipcRenderer.send("monacori:pty-resize", msg),
62
+ kill: (msg) => electron_1.ipcRenderer.send("monacori:pty-kill", msg),
63
+ onData: (cb) => {
64
+ electron_1.ipcRenderer.on("monacori:pty-data", (_event, msg) => cb(msg));
65
+ },
66
+ onExit: (cb) => {
67
+ electron_1.ipcRenderer.on("monacori:pty-exit", (_event, msg) => cb(msg));
68
+ },
69
+ });
70
+ // Global settings (locale, …) persisted by the main process under userData so they survive app
71
+ // restarts — the renderer's file:// localStorage is not reliably persisted across reopens. `all` is
72
+ // read synchronously at preload so the renderer can pick the locale before first paint; `set` writes
73
+ // asynchronously. Only present in the Electron app; browser/serve mode falls back to localStorage.
74
+ const persistedSettings = (() => {
75
+ try {
76
+ return electron_1.ipcRenderer.sendSync("monacori:get-settings") || {};
77
+ }
78
+ catch {
79
+ return {};
80
+ }
81
+ })();
82
+ electron_1.contextBridge.exposeInMainWorld("monacoriSettings", {
83
+ all: persistedSettings,
84
+ set: (key, value) => {
85
+ try {
86
+ electron_1.ipcRenderer.send("monacori:set-setting", { key, value });
87
+ }
88
+ catch {
89
+ /* noop */
90
+ }
91
+ },
92
+ });
package/dist/render.d.ts CHANGED
@@ -6,6 +6,15 @@ export declare function splitDiffForLazy(diffHtml: string, files: DiffFile[]): {
6
6
  islands: string;
7
7
  bodies: string[];
8
8
  };
9
+ export declare function renderReviewStatus(input: {
10
+ files: number;
11
+ hunks: number;
12
+ embeddedFiles: number;
13
+ sourceFileCount: number;
14
+ ignoreWhitespace?: boolean;
15
+ watch?: boolean;
16
+ generatedAt?: string;
17
+ }): string;
9
18
  export declare function renderDiffHtml(input: {
10
19
  files: DiffFile[];
11
20
  diffHtml: string;
@@ -21,9 +30,12 @@ export declare function renderDiffHtml(input: {
21
30
  projectPath: string;
22
31
  watch?: boolean;
23
32
  ignoreWhitespace?: boolean;
33
+ app?: boolean;
24
34
  signature?: string;
25
35
  generatedAt?: string;
26
36
  }): string;
37
+ export declare function renderDiffTree(files: DiffFile[]): string;
38
+ export declare function renderSourceTree(files: SourceFile[]): string;
27
39
  export declare function diffSubtitle(options: {
28
40
  base?: string;
29
41
  staged: boolean;
package/dist/render.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { escapeAttr, escapeHtml, jsonForScript } from "./util.js";
3
- import { diff2HtmlCss, diffCss, diffScript } from "./assets.js";
3
+ import { diff2HtmlCss, diffCss, diffScript, xtermCss, xtermScript } from "./assets.js";
4
+ import { MESSAGES } from "./i18n.js";
4
5
  const nodeRequire = createRequire(import.meta.url);
5
6
  const packageVersion = (() => {
6
7
  try {
@@ -81,6 +82,11 @@ export function splitDiffForLazy(diffHtml, files) {
81
82
  });
82
83
  return { container: shells.join("\n"), islands: islands.join("\n"), bodies };
83
84
  }
85
+ // The toolbar's review-status row (file/hunk counts, index + live status). Extracted so the in-place
86
+ // update path can re-render just this strip; renderDiffHtml wraps it in <div class="review-status">.
87
+ export function renderReviewStatus(input) {
88
+ return `<span>${input.files} <span data-i18n="status.files">files</span></span><span>${input.hunks} <span data-i18n="status.hunks">hunks</span></span>${input.ignoreWhitespace ? '<span class="ws-ignored" data-i18n="status.wsIgnored" data-i18n-title="status.wsIgnored.title" title="Whitespace ignored — Cmd/Ctrl+Shift+W">ws ignored</span>' : ""}<span class="index-status" id="index-status" data-i18n-title="status.index.title" title="Go-to-definition index">${input.embeddedFiles}/${input.sourceFileCount} indexed</span><span class="index-progress hidden" id="index-progress" aria-hidden="true"><span class="index-progress-bar"></span></span><span class="live-status ${input.watch ? "watching" : ""}" id="live-status"${input.watch ? ' data-i18n="status.watching"' : ""}>${input.watch ? "watching" : escapeHtml(input.generatedAt ?? new Date().toISOString())}</span>`;
89
+ }
84
90
  export function renderDiffHtml(input) {
85
91
  const totalHunks = input.files.reduce((sum, file) => sum + file.hunks.length, 0);
86
92
  const fileNav = renderDiffTree(input.files);
@@ -97,15 +103,18 @@ export function renderDiffHtml(input) {
97
103
  "<style>",
98
104
  diff2HtmlCss(),
99
105
  diffCss(),
106
+ input.app ? xtermCss() : "",
100
107
  "</style>",
101
108
  "</head>",
102
109
  "<body>",
110
+ // Boot overlay (removed by the renderer once bootstrap has painted) covers the blank gap after loadFile.
111
+ '<div id="boot-overlay"><div class="boot-spinner"></div><div>monacori</div></div>',
103
112
  '<aside class="sidebar" aria-label="Review navigation">',
104
113
  '<div class="sidebar-scroll">',
105
114
  `<div class="sidebar-brand" title="${escapeAttr(input.projectPath)}"><span class="brand-mark">monacori</span><span class="brand-project">${escapeHtml(input.projectName)}</span></div>`,
106
115
  input.lazy
107
- ? '<div class="tabs"><button type="button" class="tab active" data-tab="changes">Changes</button><button type="button" class="tab" data-tab="files">Files</button></div>'
108
- : '<div class="tabs"><button type="button" class="tab" data-tab="changes">Changes</button><button type="button" class="tab active" data-tab="files">Files</button></div>',
116
+ ? '<div class="tabs"><button type="button" class="tab active" data-tab="changes" data-i18n="tab.changes">Changes</button><button type="button" class="tab" data-tab="files" data-i18n="tab.files">Files</button></div>'
117
+ : '<div class="tabs"><button type="button" class="tab" data-tab="changes" data-i18n="tab.changes">Changes</button><button type="button" class="tab active" data-tab="files" data-i18n="tab.files">Files</button></div>',
109
118
  `<div class="tab-panel${input.lazy ? "" : " hidden"}" id="changes-panel">${fileNav}</div>`,
110
119
  // Big repos: defer the (potentially huge) source tree — ship it as an inert island, materialized on
111
120
  // the first Files-tab open, so it never builds/lays-out at startup. Small repos render it inline.
@@ -113,48 +122,119 @@ export function renderDiffHtml(input) {
113
122
  ? `<div class="tab-panel hidden" id="files-panel"></div><script type="text/html" id="files-tree-html">${sourceNav}</script>`
114
123
  : `<div class="tab-panel" id="files-panel">${sourceNav}</div>`,
115
124
  "</div>",
116
- `<div class="sidebar-footer"><span class="app-version">monacori${packageVersion ? " v" + escapeHtml(packageVersion) : ""}</span><span id="app-update-flag" class="app-update-flag hidden" title="Update available">update available</span><button type="button" id="app-info-btn" class="settings-btn" aria-haspopup="dialog" aria-label="About monacori" title="About monacori">⚙</button></div>`,
125
+ `<div class="sidebar-footer"><span class="app-version">monacori${packageVersion ? " v" + escapeHtml(packageVersion) : ""}</span><span id="app-update-flag" class="app-update-flag hidden" data-i18n="sidebar.updateAvailable" data-i18n-title="settings.updateAvailable" title="Update available">update available</span><button type="button" id="terminal-toggle" class="settings-btn terminal-toggle hidden" data-i18n-title="terminal.toggle" title="Toggle terminal (Ctrl+\`)" aria-label="Toggle terminal"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7l4 5-4 5"/><path d="M13 17h6"/></svg></button><button type="button" id="app-info-btn" class="settings-btn" aria-haspopup="dialog" data-i18n-aria="about.title" data-i18n-title="about.title" aria-label="About monacori" title="About monacori">⚙</button></div>`,
117
126
  "</aside>",
118
127
  '<div class="sidebar-resizer" aria-hidden="true"></div>',
119
128
  '<main class="content">',
120
129
  '<section id="diff-view" class="hidden">',
121
130
  '<div class="toolbar">',
122
131
  '<div class="breadcrumb" id="diff-breadcrumb"></div>',
123
- `<div class="review-status"><span>${input.files.length} files</span><span>${totalHunks} hunks</span>${input.ignoreWhitespace ? '<span class="ws-ignored" title="Whitespace ignored — Cmd/Ctrl+Shift+W">ws ignored</span>' : ""}<span class="index-status" id="index-status" title="Go-to-definition index">${embeddedFiles}/${input.sourceFiles.length} indexed</span><span class="index-progress hidden" id="index-progress" aria-hidden="true"><span class="index-progress-bar"></span></span><span class="live-status ${input.watch ? "watching" : ""}" id="live-status">${input.watch ? "watching" : escapeHtml(input.generatedAt ?? new Date().toISOString())}</span></div>`,
124
- '<button type="button" id="diff-viewed-toggle" class="diff-viewed-toggle" aria-pressed="false" title="Toggle viewed (<)" hidden>Viewed</button>',
132
+ `<div class="review-status">${renderReviewStatus({ files: input.files.length, hunks: totalHunks, embeddedFiles, sourceFileCount: input.sourceFiles.length, ignoreWhitespace: input.ignoreWhitespace, watch: input.watch, generatedAt: input.generatedAt })}</div>`,
133
+ '<button type="button" id="diff-viewed-toggle" class="diff-viewed-toggle" aria-pressed="false" data-i18n="btn.viewed" data-i18n-title="btn.viewed.title" title="Toggle viewed (<)" hidden>Viewed</button>',
125
134
  "</div>",
126
- `<div id="diff2html-container" class="diff2html-container">${input.diffHtml || '<div class="empty">No diff to review.</div>'}</div>`,
135
+ `<div id="diff2html-container" class="diff2html-container">${input.diffHtml || '<div class="empty" data-i18n="diff.noDiff">No diff to review.</div>'}</div>`,
127
136
  "</section>",
128
137
  '<section id="source-viewer" class="source-viewer">',
138
+ '<div id="source-tabs" class="source-tabs hidden" role="tablist"></div>',
129
139
  '<div class="toolbar source-toolbar">',
130
- '<div class="source-file-meta"><span id="source-type-icon" class="source-type-icon" aria-hidden="true"></span><span id="source-title">Source</span><span id="source-meta">Select a file from the Files tab.</span></div>',
131
- '<select id="http-env-select" class="http-env-select hidden" title="HTTP Client environment" aria-label="HTTP environment"></select>',
132
- '<button type="button" id="back-to-diff" class="plain-button">Diff</button>',
140
+ '<div class="source-file-meta"><span id="source-type-icon" class="source-type-icon" aria-hidden="true"></span><span id="source-title" data-i18n="source.title">Source</span><span id="source-meta" data-i18n="source.selectFile">Select a file from the Files tab.</span></div>',
141
+ '<select id="http-env-select" class="http-env-select hidden" data-i18n-title="http.env.title" data-i18n-aria="http.env.aria" title="HTTP Client environment" aria-label="HTTP environment"></select>',
142
+ '<button type="button" id="render-toggle" class="plain-button hidden" aria-pressed="false">Raw</button>',
143
+ '<button type="button" id="back-to-diff" class="plain-button" data-i18n="btn.diff">Diff</button>',
133
144
  "</div>",
134
- '<div id="source-body" class="source-body empty">Select a file from the Files tab.</div>',
145
+ '<div id="source-body" class="source-body empty" data-i18n="source.selectFile">Select a file from the Files tab.</div>',
135
146
  "</section>",
136
147
  "</main>",
137
- `<div id="app-info" class="app-info hidden" role="dialog" aria-modal="false" aria-label="About monacori"><div class="app-info-head">monacori <span class="app-info-ver">${packageVersion ? "v" + escapeHtml(packageVersion) : ""}</span></div><div id="app-info-status" class="app-info-status">Checking for updates…</div><div class="app-info-cmd"><code>npm i -g @happy-nut/monacori</code><button type="button" id="app-info-copy" class="plain-button">Copy</button></div><div class="app-info-keys"><div class="app-info-keys-h">Keyboard shortcuts</div><div class="keys-grid"><kbd>F7</kbd><span>Next change</span><kbd>Shift+F7</kbd><span>Previous change</span><kbd>Cmd/Ctrl+[ / ]</kbd><span>Cursor back / forward</span><kbd>Shift Shift</kbd><span>Find file</span><kbd>Cmd/Ctrl+Shift+F</kbd><span>Find in files</span><kbd>Cmd/Ctrl+E</kbd><span>Recent files</span><kbd>Cmd/Ctrl+B</kbd><span>Definition / usages</span><kbd>Cmd/Ctrl+&darr;</kbd><span>Go to definition</span><kbd>Cmd/Ctrl+1 / 0</kbd><span>Files / Changes tab</span><kbd>Tab</kbd><span>Sidebar &harr; content</span><kbd>Opt/Alt+&larr;/&rarr;</kbd><span>Word jump (vim w)</span><kbd>Cmd/Ctrl+&larr;/&rarr;</kbd><span>Line start / end</span><kbd>Shift+arrows</kbd><span>Extend selection</span><kbd>&lt;</kbd><span>Toggle viewed</span><kbd>? &nbsp;&gt;</kbd><span>Add question / change</span><kbd>Cmd/Ctrl+Shift+/ .</kbd><span>All questions / changes</span><kbd>Cmd/Ctrl+Shift+W</kbd><span>Ignore whitespace</span><kbd>Cmd/Ctrl+Enter</kbd><span>Save comment</span></div></div></div>`,
138
- '<div id="quick-open" class="quick-open hidden" role="dialog" aria-modal="true" aria-label="Quick open">',
148
+ // Integrated terminal panel (Electron only shown when window.monacoriPty exists). Fixed to the
149
+ // content column's bottom; a top resizer drags its height. The merged prompt is sent here.
150
+ input.app
151
+ ? '<div id="terminal-panel" class="terminal-panel hidden"><div class="terminal-resizer" aria-hidden="true"></div><div class="terminal-bar"><span class="terminal-title" data-i18n="terminal.title">Terminal</span><button type="button" id="terminal-close" class="terminal-x" data-i18n-title="terminal.close" title="Close terminal" aria-label="Close terminal">&times;</button></div><div id="terminal-host" class="terminal-host"></div></div>'
152
+ : "",
153
+ '<div id="quick-open" class="quick-open hidden" role="dialog" aria-modal="true" data-i18n-aria="quickopen.aria" aria-label="Quick open">',
139
154
  '<div class="quick-open-panel">',
140
- '<div class="quick-open-title"><span id="quick-open-mode">Search files</span></div>',
141
- '<input id="quick-open-input" type="search" autocomplete="off" spellcheck="false" placeholder="Search files">',
155
+ '<div class="quick-open-title"><span id="quick-open-mode" data-i18n="quickopen.searchFiles">Search files</span></div>',
156
+ '<input id="quick-open-input" type="search" autocomplete="off" spellcheck="false" data-i18n-ph="quickopen.searchFiles" placeholder="Search files">',
142
157
  '<div id="quick-open-results" class="quick-open-results"></div>',
143
158
  '<div id="quick-open-preview" class="quick-open-preview"></div>',
144
159
  "</div>",
145
160
  "</div>",
146
- '<div id="usages" class="quick-open hidden" role="dialog" aria-modal="true" aria-label="Usages">',
161
+ '<div id="usages" class="quick-open hidden" role="dialog" aria-modal="true" data-i18n-aria="usages.aria" aria-label="Usages">',
147
162
  '<div class="quick-open-panel">',
148
- '<div class="quick-open-title"><span id="usages-title">Usages</span></div>',
163
+ '<div class="quick-open-title"><span id="usages-title" data-i18n="usages.title">Usages</span></div>',
149
164
  '<div id="usages-results" class="quick-open-results"></div>',
150
165
  "</div>",
151
166
  "</div>",
167
+ '<div id="settings-modal" class="settings-modal hidden" role="dialog" aria-modal="true" data-i18n-aria="settings.aria" aria-label="Settings">',
168
+ '<div class="settings-panel">',
169
+ '<aside class="settings-nav"><div class="settings-nav-title" data-i18n="settings.title">Settings</div><button type="button" class="settings-cat active" data-cat="general" data-i18n="settings.cat.general">General</button><button type="button" class="settings-cat" data-cat="prompts" data-i18n="settings.cat.prompts">Merge prompts</button></aside>',
170
+ '<div class="settings-body">',
171
+ '<section class="settings-section" data-cat="general">',
172
+ `<div class="settings-h">monacori <span class="settings-ver">${packageVersion ? "v" + escapeHtml(packageVersion) : ""}</span></div>`,
173
+ '<div id="app-info-status" class="app-info-status" data-i18n="settings.checkingUpdates">Checking for updates…</div>',
174
+ '<button type="button" id="app-info-update" class="plain-button app-info-update hidden" data-i18n="settings.updateRestart">Update &amp; Restart</button>',
175
+ '<label class="settings-label" for="settings-language" data-i18n="settings.language">Language</label>',
176
+ '<select id="settings-language" class="settings-select"><option value="en">English</option><option value="ko">한국어</option></select>',
177
+ '<div class="app-info-keys">' +
178
+ '<div class="app-info-keys-h" data-i18n="settings.kbd.title">Keyboard shortcuts</div>' +
179
+ '<div class="keys-cat" data-i18n="settings.kbd.cat.nav">Navigation</div>' +
180
+ '<div class="keys-grid">' +
181
+ '<kbd>F7</kbd><span data-i18n="kbd.nextChange">Next change</span>' +
182
+ '<kbd>Shift+F7</kbd><span data-i18n="kbd.prevChange">Previous change</span>' +
183
+ '<kbd>Cmd/Ctrl+1 / 0</kbd><span data-i18n="kbd.filesChangesTab">Files / Changes tab</span>' +
184
+ '<kbd>Tab</kbd><span data-i18n="kbd.sidebarContent">Sidebar &harr; content</span>' +
185
+ '<kbd>Shift Shift</kbd><span data-i18n="kbd.findFile">Find file</span>' +
186
+ '<kbd>Cmd/Ctrl+Shift+F</kbd><span data-i18n="kbd.findInFiles">Find in files</span>' +
187
+ '<kbd>Cmd/Ctrl+E</kbd><span data-i18n="kbd.recentFiles">Recent files</span>' +
188
+ '<kbd>Cmd/Ctrl+B</kbd><span data-i18n="kbd.defUsages">Definition / usages</span>' +
189
+ '<kbd>Cmd/Ctrl+&darr;</kbd><span data-i18n="kbd.goToDef">Go to definition</span>' +
190
+ '<kbd>Cmd/Ctrl+Shift+[ / ]</kbd><span data-i18n="kbd.prevNextTab">Prev / next tab</span>' +
191
+ '<kbd>Cmd/Ctrl+[ / ]</kbd><span data-i18n="kbd.cursorBackForward">Cursor back / forward</span>' +
192
+ '<kbd>Opt/Alt+&larr;/&rarr;</kbd><span data-i18n="kbd.wordJump">Word jump (vim w)</span>' +
193
+ '<kbd>Cmd/Ctrl+&larr;/&rarr;</kbd><span data-i18n="kbd.lineStartEnd">Line start / end</span>' +
194
+ '<kbd>Shift+arrows</kbd><span data-i18n="kbd.extendSelection">Extend selection</span>' +
195
+ '<kbd>Cmd/Ctrl+W</kbd><span data-i18n="kbd.closeTab">Close tab</span>' +
196
+ '</div>' +
197
+ '<div class="keys-cat" data-i18n="settings.kbd.cat.review">Review</div>' +
198
+ '<div class="keys-grid">' +
199
+ '<kbd>&lt;</kbd><span data-i18n="kbd.toggleViewed">Toggle viewed</span>' +
200
+ '<kbd>? &nbsp;&gt;</kbd><span data-i18n="kbd.addQuestionChange">Add question / change</span>' +
201
+ '<kbd>Cmd/Ctrl+Shift+/ .</kbd><span data-i18n="kbd.allQuestionsChanges">All questions / changes</span>' +
202
+ '<kbd>Cmd/Ctrl+Shift+W</kbd><span data-i18n="kbd.ignoreWhitespace">Ignore whitespace</span>' +
203
+ '<kbd>Cmd/Ctrl+Enter</kbd><span data-i18n="kbd.saveComment">Save comment</span>' +
204
+ '<kbd>Cmd/Ctrl+Shift+N</kbd><span data-i18n="kbd.promptMemo">Prompt memo</span>' +
205
+ '</div>' +
206
+ '<div class="keys-cat" data-i18n="settings.kbd.cat.terminal">Terminal</div>' +
207
+ '<div class="keys-grid">' +
208
+ '<kbd>Ctrl+`</kbd><span data-i18n="kbd.toggleTerminal">Toggle terminal</span>' +
209
+ '<kbd>Cmd/Ctrl+D</kbd><span data-i18n="kbd.splitPane">Split pane</span>' +
210
+ '<kbd>Cmd/Ctrl+Alt+[ / ]</kbd><span data-i18n="kbd.focusPane">Focus prev / next pane</span>' +
211
+ '<kbd>Cmd/Ctrl+Alt+R</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
212
+ '<kbd>Cmd/Ctrl+W</kbd><span data-i18n="kbd.closeTerminal">Close terminal (when focused)</span>' +
213
+ '</div>' +
214
+ '</div>',
215
+ "</section>",
216
+ '<section class="settings-section hidden" data-cat="prompts">',
217
+ '<div class="settings-h" data-i18n="mergePrompts.title">Merge prompts</div>',
218
+ '<div class="settings-desc" data-i18n="mergePrompts.desc">Heading prepended to the merged prompt opened with Cmd/Ctrl+Shift+/ (questions) and Cmd/Ctrl+Shift+. (change requests). Leave blank to use the default.</div>',
219
+ '<label class="settings-label" for="settings-prompt-q" data-i18n="mergePrompts.qHeading">Questions heading</label>',
220
+ '<textarea id="settings-prompt-q" class="settings-textarea" rows="4" spellcheck="false"></textarea>',
221
+ '<label class="settings-label" for="settings-prompt-c" data-i18n="mergePrompts.cHeading">Change-requests heading</label>',
222
+ '<textarea id="settings-prompt-c" class="settings-textarea" rows="4" spellcheck="false"></textarea>',
223
+ '<div class="settings-actions"><button type="button" id="settings-reset" class="plain-button" data-i18n="mergePrompts.reset">Reset to defaults</button><span id="settings-saved" class="settings-saved"></span></div>',
224
+ "</section>",
225
+ "</div>",
226
+ "</div>",
227
+ "</div>",
152
228
  input.diffIslands || "",
153
229
  `<script type="application/json" id="review-meta" data-watch="${input.watch ? "true" : "false"}" data-signature="${escapeAttr(input.signature ?? "")}" data-generated-at="${escapeAttr(input.generatedAt ?? "")}" data-lazy="${input.lazy ? "true" : "false"}" data-lazy-load="${input.lazyLoad ? "true" : "false"}">{}</script>`,
230
+ `<script type="application/json" id="i18n-data">${jsonForScript(MESSAGES)}</script>`,
154
231
  `<script type="application/json" id="source-files-data">${jsonForScript(input.lazyLoad ? input.sourceFiles.map((f) => ({ ...f, content: "", image: "" })) : input.sourceFiles)}</script>`,
155
232
  `<script type="application/json" id="file-state-data">${jsonForScript(input.fileStates)}</script>`,
156
233
  `<script type="application/json" id="http-env-data">${jsonForScript(input.httpEnvironments)}</script>`,
157
234
  `<script>window.__MONACORI_VERSION__=${JSON.stringify(packageVersion)};</script>`,
235
+ // xterm ships as an inert island (type=text/html, not parsed/compiled at startup) and is injected on
236
+ // the first terminal open — ~490KB the renderer would otherwise parse on every launch even unused.
237
+ input.app ? `<script type="text/html" id="xterm-code">${xtermScript()}</script>` : "",
158
238
  "<script>",
159
239
  diffScript(),
160
240
  "</script>",
@@ -162,7 +242,7 @@ export function renderDiffHtml(input) {
162
242
  "</html>",
163
243
  ].join("\n");
164
244
  }
165
- function renderDiffTree(files) {
245
+ export function renderDiffTree(files) {
166
246
  if (files.length === 0) {
167
247
  return '<div class="empty-nav">No changed files</div>';
168
248
  }
@@ -184,7 +264,7 @@ function renderDiffTree(files) {
184
264
  const name = slash >= 0 ? file.displayPath.slice(slash + 1) : file.displayPath;
185
265
  const dir = slash > 0 ? file.displayPath.slice(0, slash) : "";
186
266
  return [
187
- `<a class="file-link change-row" href="#file-${fileIndex}" data-hunk="${firstHunk}" data-file="${escapeAttr(file.displayPath)}" title="${escapeAttr(file.displayPath + " — " + file.status)}">`,
267
+ `<a class="file-link change-row${file.vcs ? " vcs-" + file.vcs : ""}" href="#file-${fileIndex}" data-hunk="${firstHunk}" data-file="${escapeAttr(file.displayPath)}" title="${escapeAttr(file.displayPath + " — " + file.status)}">`,
188
268
  fileTypeIcon(file.displayPath),
189
269
  `<span class="status status-${escapeAttr(file.status)}">${escapeHtml(file.status)}</span>`,
190
270
  `<span class="change-name"><span class="path" title="${escapeAttr(file.displayPath)}">${escapeHtml(name)}</span>${dir ? `<span class="change-dir">${escapeHtml(dir)}</span>` : ""}</span>`,
@@ -194,7 +274,7 @@ function renderDiffTree(files) {
194
274
  });
195
275
  return `<nav class="tree changes-flat">${rows.join("")}</nav>`;
196
276
  }
197
- function renderSourceTree(files) {
277
+ export function renderSourceTree(files) {
198
278
  if (files.length === 0) {
199
279
  return '<div class="empty-nav">No source files indexed</div>';
200
280
  }
@@ -302,7 +382,7 @@ function fileTypeIcon(path) {
302
382
  function renderSourceNode(node, depth) {
303
383
  if (node.file) {
304
384
  const file = node.file;
305
- const classes = ["file-link", "source-link", "tree-file", file.embedded ? "" : "not-embedded"].filter(Boolean).join(" ");
385
+ const classes = ["file-link", "source-link", "tree-file", file.embedded ? "" : "not-embedded", file.vcs ? "vcs-" + file.vcs : ""].filter(Boolean).join(" ");
306
386
  const tip = file.path + (file.embedded ? "" : " — not embedded");
307
387
  return [
308
388
  `<button type="button" class="${classes}" data-source-file="${escapeAttr(file.path)}" style="--depth:${depth}" title="${escapeAttr(tip)}">`,
@@ -322,7 +402,7 @@ function renderSourceNode(node, depth) {
322
402
  }
323
403
  return [
324
404
  `<details class="tree-dir source-dir" data-dir="${escapeAttr(labelNode.path)}" style="--depth:${depth}">`,
325
- `<summary><span class="folder-icon">v</span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
405
+ `<summary><span class="folder-icon"><svg class="folder-ic fi-closed" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg><svg class="folder-ic fi-open" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 14 1.45-2.9A2 2 0 0 1 9.24 10H21a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg></span><span class="path">${escapeHtml(names.join("/"))}</span></summary>`,
326
406
  renderSourceChildren(labelNode, depth + 1),
327
407
  "</details>",
328
408
  ].join("\n");