@happy-nut/monacori 0.1.21 → 0.1.22

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/app-main.js CHANGED
@@ -2,9 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage } from "electron";
5
+ import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, Notification } from "electron";
6
6
  import { buildDiffReview, performHttpRequest } from "./cli.js";
7
- import { sanitizeTerminalEnv } from "./util.js";
7
+ import { sanitizeTerminalEnv, ensureUtf8Locale } from "./util.js";
8
8
  import { readUnifiedDiff } from "./diff.js";
9
9
  import { isGitRepository } from "./git.js";
10
10
  import { renderWelcomeHtml } from "./render.js";
@@ -190,11 +190,13 @@ ipcMain.handle("monacori:pty-spawn", (event, size) => {
190
190
  const id = ++nextPtyId;
191
191
  const shell = process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/zsh");
192
192
  const t = spawnPty(shell, [], {
193
- name: "xterm-color",
193
+ // 256-color terminfo + COLORTERM=truecolor so TUIs (e.g. Claude Code's coral logo) emit 24-bit color and
194
+ // xterm.js renders the exact hue. "xterm-color" is 8-color, which downgraded the orange logo to ANSI red.
195
+ name: "xterm-256color",
194
196
  cols: size?.cols ?? 80,
195
197
  rows: size?.rows ?? 24,
196
198
  cwd: state.options.root,
197
- env: sanitizeTerminalEnv(process.env),
199
+ env: ensureUtf8Locale({ ...sanitizeTerminalEnv(process.env), TERM: "xterm-256color", COLORTERM: "truecolor" }),
198
200
  });
199
201
  state.terms.set(id, t);
200
202
  // Guard every relay with isDestroyed(): a pty can outlive its window (close races pty teardown), and
@@ -227,6 +229,35 @@ ipcMain.on("monacori:pty-kill", (event, msg) => {
227
229
  state.terms.delete(msg.id);
228
230
  }
229
231
  });
232
+ // A TUI in the integrated terminal rang the bell (e.g. Claude Code finished a turn / needs input). Raise a
233
+ // native notification when the window ISN'T focused — while you're watching, the bell itself is enough — plus
234
+ // a dock bounce / taskbar flash. Clicking the notification brings the window forward.
235
+ ipcMain.on("monacori:bell", (event, msg) => {
236
+ const win = BrowserWindow.fromWebContents(event.sender);
237
+ if (!win || win.isDestroyed() || win.isFocused())
238
+ return;
239
+ try {
240
+ if (Notification.isSupported()) {
241
+ const note = new Notification({ title: msg?.title || "monacori", body: msg?.body || "Terminal task finished" });
242
+ note.on("click", () => { if (!win.isDestroyed()) {
243
+ win.show();
244
+ win.focus();
245
+ } });
246
+ note.show();
247
+ }
248
+ }
249
+ catch { /* notifications are best-effort */ }
250
+ try {
251
+ win.flashFrame(true);
252
+ }
253
+ catch { /* taskbar flash — Windows/Linux */ }
254
+ if (process.platform === "darwin" && app.dock) {
255
+ try {
256
+ app.dock.bounce("informational");
257
+ }
258
+ catch { /* best-effort */ }
259
+ }
260
+ });
230
261
  // Persisted global settings (locale, …) live in a JSON file under userData and reach the renderer
231
262
  // via preload + the two handlers below. The renderer's file:// localStorage is NOT reliably persisted
232
263
  // across app restarts, so settings that must survive a reopen round-trip through the main process.
@@ -306,6 +337,10 @@ app.on("window-all-closed", () => {
306
337
  });
307
338
  // Keep the Ignore-whitespace menu checkbox honest as focus moves between windows (it's per-window state).
308
339
  app.on("browser-window-focus", (_event, win) => {
340
+ try {
341
+ win.flashFrame(false);
342
+ }
343
+ catch { /* stop any terminal-bell taskbar flash once the user is back */ }
309
344
  const state = states.get(win.id);
310
345
  const item = Menu.getApplicationMenu()?.getMenuItemById("ignore-whitespace");
311
346
  if (item && state)
package/dist/assets.js CHANGED
@@ -26,7 +26,14 @@ export function diffCss() {
26
26
  return readViewerAsset("viewer.css");
27
27
  }
28
28
  export function diffScript() {
29
- return readViewerAsset("viewer.client.js");
29
+ // Prefer the minified bundle the build emits (smaller inlined <script>); fall back to the readable concat
30
+ // when minify was skipped (e.g. terser unavailable).
31
+ try {
32
+ return readViewerAsset("viewer.client.min.js");
33
+ }
34
+ catch {
35
+ return readViewerAsset("viewer.client.js");
36
+ }
30
37
  }
31
38
  // xterm.js (terminal renderer) for the integrated terminal panel. UMD bundles that expose
32
39
  // window.Terminal + window.FitAddon when inlined. Resolved from node_modules like diff2HtmlCss();
package/dist/build.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { basename } from "node:path";
3
- import { isGitRepository } from "./git.js";
3
+ import { isGitRepository, git } from "./git.js";
4
4
  import { collectHttpEnvironments, collectReviewFileStates, collectSourceFiles, parseUnifiedDiff, readUnifiedDiff } from "./diff.js";
5
5
  import { renderDiff2Html } from "./highlight.js";
6
6
  import { diffSubtitle, renderDiffHtml, renderDiffTree, renderNotGitRepoHtml, renderReviewStatus, renderSourceTree, shouldLazyRender, splitDiffForLazy } from "./render.js";
@@ -29,6 +29,8 @@ export function buildDiffReview(input) {
29
29
  const httpEnvironments = collectHttpEnvironments(root);
30
30
  const hunks = files.reduce((sum, file) => sum + file.hunks.length, 0);
31
31
  const generatedAt = new Date().toISOString();
32
+ // Current branch for the sidebar chip (empty on a detached HEAD); refreshed in the watch payload too.
33
+ const branch = git(root, ["branch", "--show-current"]);
32
34
  const diffHtml = renderDiff2Html(diffText);
33
35
  const totalLines = files.reduce((sum, file) => sum + file.hunks.reduce((t, h) => t + h.lines.length, 0), 0);
34
36
  // lazy-LOAD (Phase 2) serves each file body + source on demand instead of embedding them; it implies
@@ -61,6 +63,7 @@ export function buildDiffReview(input) {
61
63
  subtitle: diffSubtitle(input),
62
64
  projectName: basename(root),
63
65
  projectPath: root,
66
+ branch,
64
67
  watch: Boolean(input.watch),
65
68
  ignoreWhitespace: Boolean(input.ignoreWhitespace),
66
69
  app: Boolean(input.app),
@@ -72,6 +75,7 @@ export function buildDiffReview(input) {
72
75
  const update = {
73
76
  signature,
74
77
  generatedAt,
78
+ branch,
75
79
  diffContainer: diffSplit.container || '<div class="empty" data-i18n="diff.noDiff">No diff to review.</div>',
76
80
  changesPanel: renderDiffTree(files),
77
81
  filesTree: renderSourceTree(sourceFiles),
package/dist/i18n.js CHANGED
@@ -14,19 +14,25 @@ export const MESSAGES = {
14
14
  // Tabs (sidebar)
15
15
  "tab.changes": "Changes",
16
16
  "tab.files": "Files",
17
+ "tab.changes.title": "Changes (⌘0)",
18
+ "tab.files.title": "Files (⌘1)",
19
+ "rail.questions": "Questions",
20
+ "rail.changeRequests": "Change requests",
21
+ "rail.branch": "Current branch",
17
22
  // Sidebar footer / About
18
23
  "sidebar.updateAvailable": "update available",
19
24
  "about.title": "About monacori",
25
+ "about.tip": "Settings (⌘,)",
20
26
  "terminal.title": "Terminal",
21
- "terminal.toggle": "Toggle terminal (Ctrl+`)",
27
+ "terminal.toggle": "Toggle terminal (⌃`)",
22
28
  "terminal.close": "Close terminal",
23
- "dock.maximize": "Maximize panel (Cmd/Ctrl+Shift+')",
24
- "dock.restore": "Restore panel (Cmd/Ctrl+Shift+')",
29
+ "dock.maximize": "Maximize panel (⌘⇧')",
30
+ "dock.restore": "Restore panel (⌘⇧')",
25
31
  // Review status (toolbar) — units; the numeric count stays dynamic and is prepended at runtime.
26
32
  "status.files": "files",
27
33
  "status.hunks": "hunks",
28
34
  "status.wsIgnored": "ws ignored",
29
- "status.wsIgnored.title": "Whitespace ignored — Cmd/Ctrl+Shift+W",
35
+ "status.wsIgnored.title": "Whitespace ignored — ⌘⇧W",
30
36
  "status.indexed": "indexed",
31
37
  "status.index.title": "Go-to-definition index",
32
38
  "status.indexing": "indexing",
@@ -44,6 +50,7 @@ export const MESSAGES = {
44
50
  "http.env.title": "HTTP Client environment",
45
51
  "http.env.aria": "HTTP environment",
46
52
  "btn.diff": "Diff",
53
+ "btn.diff.title": "Back to diff (F7)",
47
54
  "source.loading": "Loading source…",
48
55
  "source.previewUnavailable": "Source preview unavailable.",
49
56
  "source.viewRaw": "Raw",
@@ -55,6 +62,7 @@ export const MESSAGES = {
55
62
  "quickopen.recent": "Recent files",
56
63
  "quickopen.findInFiles": "Find in Files",
57
64
  "quickopen.noFiles": "No files found.",
65
+ "quickopen.typeToFilter": "type to filter",
58
66
  // Usages
59
67
  "usages.aria": "Usages",
60
68
  "usages.title": "Usages",
@@ -66,6 +74,8 @@ export const MESSAGES = {
66
74
  // Settings — General
67
75
  "settings.language": "Language",
68
76
  "settings.theme": "Theme",
77
+ "settings.bellNotify": "Notify when a terminal task finishes (bell)",
78
+ "notify.bellBody": "a task finished or needs your input",
69
79
  "theme.dark": "Dark",
70
80
  "theme.light": "Light",
71
81
  "settings.checkingUpdates": "Checking for updates…",
@@ -76,10 +86,21 @@ export const MESSAGES = {
76
86
  "settings.updated": "Updated. Restarting…",
77
87
  "settings.updateFailed": "Update failed — try again, or run: npm i -g @happy-nut/monacori",
78
88
  "settings.kbd.title": "Keyboard shortcuts",
89
+ "settings.kbd.cat.app": "App",
79
90
  "settings.kbd.cat.nav": "Navigation",
80
91
  "settings.kbd.cat.review": "Review",
81
92
  "settings.kbd.cat.terminal": "Terminal",
82
93
  // Settings — keyboard-shortcut labels (descriptions only; <kbd> key names stay literal)
94
+ "kbd.openFolder": "Open folder",
95
+ "kbd.openNewWindow": "Open in new window",
96
+ "kbd.openSettings": "Settings",
97
+ "kbd.closeDialog": "Close dialog / cancel",
98
+ "kbd.pageUpDown": "Page up / down",
99
+ "kbd.runHttp": "Run HTTP request (.http)",
100
+ "kbd.editComment": "Edit comment (when selected)",
101
+ "kbd.deleteComment": "Delete comment (when selected)",
102
+ "kbd.stepComments": "Step between comments (merged)",
103
+ "kbd.mergedSend": "Comment menu / send to pane (merged)",
83
104
  "kbd.nextChange": "Next change",
84
105
  "kbd.prevChange": "Previous change",
85
106
  "kbd.closeTab": "Close tab",
@@ -109,7 +130,7 @@ export const MESSAGES = {
109
130
  "kbd.closeTerminal": "Close terminal (when focused)",
110
131
  // Settings — Merge prompts
111
132
  "mergePrompts.title": "Merge prompts",
112
- "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.",
133
+ "mergePrompts.desc": "Heading prepended to the merged prompt opened with ⌘⇧/ (questions) and ⌘⇧. (change requests). Leave blank to use the default.",
113
134
  "mergePrompts.qHeading": "Questions heading",
114
135
  "mergePrompts.cHeading": "Change-requests heading",
115
136
  "mergePrompts.reset": "Reset to defaults",
@@ -119,7 +140,7 @@ export const MESSAGES = {
119
140
  "composer.changeRequest": "Request a change for this line",
120
141
  "composer.save": "Comment",
121
142
  "composer.cancel": "Cancel",
122
- "composer.hint": "Cmd/Ctrl+Enter to save, Esc to cancel",
143
+ "composer.hint": "Enter to save, Esc to cancel",
123
144
  "composer.delete": "Delete",
124
145
  "comment.kind.q": "❓ Question",
125
146
  "comment.kind.c": "✎ Change request",
@@ -149,19 +170,25 @@ export const MESSAGES = {
149
170
  // Tabs (sidebar)
150
171
  "tab.changes": "변경사항",
151
172
  "tab.files": "파일",
173
+ "rail.questions": "질문",
174
+ "rail.changeRequests": "변경 요청",
175
+ "rail.branch": "현재 브랜치",
176
+ "tab.changes.title": "변경사항 (⌘0)",
177
+ "tab.files.title": "파일 (⌘1)",
152
178
  // Sidebar footer / About
153
179
  "sidebar.updateAvailable": "업데이트 있음",
154
180
  "about.title": "monacori 정보",
181
+ "about.tip": "설정 (⌘,)",
155
182
  "terminal.title": "터미널",
156
- "terminal.toggle": "터미널 토글 (Ctrl+`)",
183
+ "terminal.toggle": "터미널 토글 (⌃`)",
157
184
  "terminal.close": "터미널 닫기",
158
- "dock.maximize": "패널 최대화 (Cmd/Ctrl+Shift+')",
159
- "dock.restore": "패널 복원 (Cmd/Ctrl+Shift+')",
185
+ "dock.maximize": "패널 최대화 (⌘⇧')",
186
+ "dock.restore": "패널 복원 (⌘⇧')",
160
187
  // Review status (toolbar)
161
188
  "status.files": "개 파일",
162
189
  "status.hunks": "개 변경 묶음",
163
190
  "status.wsIgnored": "공백 무시",
164
- "status.wsIgnored.title": "공백 무시 — Cmd/Ctrl+Shift+W",
191
+ "status.wsIgnored.title": "공백 무시 — ⌘⇧W",
165
192
  "status.indexed": "개 인덱싱됨",
166
193
  "status.index.title": "정의로 이동 인덱스",
167
194
  "status.indexing": "인덱싱 중",
@@ -179,6 +206,7 @@ export const MESSAGES = {
179
206
  "http.env.title": "HTTP 클라이언트 환경",
180
207
  "http.env.aria": "HTTP 환경",
181
208
  "btn.diff": "Diff",
209
+ "btn.diff.title": "Diff로 돌아가기 (F7)",
182
210
  "source.loading": "소스 불러오는 중…",
183
211
  "source.previewUnavailable": "소스 미리보기를 사용할 수 없습니다.",
184
212
  "source.viewRaw": "원문",
@@ -190,6 +218,7 @@ export const MESSAGES = {
190
218
  "quickopen.recent": "최근 파일",
191
219
  "quickopen.findInFiles": "파일 내용 검색",
192
220
  "quickopen.noFiles": "파일을 찾을 수 없습니다.",
221
+ "quickopen.typeToFilter": "입력하여 필터",
193
222
  // Usages
194
223
  "usages.aria": "사용처",
195
224
  "usages.title": "사용처",
@@ -201,6 +230,8 @@ export const MESSAGES = {
201
230
  // Settings — General
202
231
  "settings.language": "언어",
203
232
  "settings.theme": "테마",
233
+ "settings.bellNotify": "터미널 작업이 끝나면 알림 (벨)",
234
+ "notify.bellBody": "작업이 끝났거나 입력이 필요합니다",
204
235
  "theme.dark": "다크",
205
236
  "theme.light": "라이트",
206
237
  "settings.checkingUpdates": "업데이트 확인 중…",
@@ -211,10 +242,21 @@ export const MESSAGES = {
211
242
  "settings.updated": "업데이트 완료. 재시작 중…",
212
243
  "settings.updateFailed": "업데이트 실패 — 다시 시도하거나 실행하세요: npm i -g @happy-nut/monacori",
213
244
  "settings.kbd.title": "키보드 단축키",
245
+ "settings.kbd.cat.app": "앱",
214
246
  "settings.kbd.cat.nav": "탐색",
215
247
  "settings.kbd.cat.review": "리뷰",
216
248
  "settings.kbd.cat.terminal": "터미널",
217
249
  // Settings — keyboard-shortcut labels
250
+ "kbd.openFolder": "폴더 열기",
251
+ "kbd.openNewWindow": "새 창에서 열기",
252
+ "kbd.openSettings": "설정",
253
+ "kbd.closeDialog": "대화상자 닫기 / 취소",
254
+ "kbd.pageUpDown": "페이지 위 / 아래",
255
+ "kbd.runHttp": "HTTP 요청 실행 (.http)",
256
+ "kbd.editComment": "코멘트 편집 (선택 시)",
257
+ "kbd.deleteComment": "코멘트 삭제 (선택 시)",
258
+ "kbd.stepComments": "코멘트 단위 이동 (합본)",
259
+ "kbd.mergedSend": "코멘트 메뉴 / 패널로 전송 (합본)",
218
260
  "kbd.nextChange": "다음 변경",
219
261
  "kbd.prevChange": "이전 변경",
220
262
  "kbd.closeTab": "탭 닫기",
@@ -244,7 +286,7 @@ export const MESSAGES = {
244
286
  "kbd.closeTerminal": "터미널 닫기 (포커스 시)",
245
287
  // Settings — Merge prompts
246
288
  "mergePrompts.title": "병합 프롬프트",
247
- "mergePrompts.desc": "Cmd/Ctrl+Shift+/ (질문) 및 Cmd/Ctrl+Shift+. (변경요청)로 여는 병합 프롬프트 맨 앞에 붙는 머리말입니다. 비워 두면 기본값을 사용합니다.",
289
+ "mergePrompts.desc": "⌘⇧/ (질문) 및 ⌘⇧. (변경요청)로 여는 병합 프롬프트 맨 앞에 붙는 머리말입니다. 비워 두면 기본값을 사용합니다.",
248
290
  "mergePrompts.qHeading": "질문 머리말",
249
291
  "mergePrompts.cHeading": "변경요청 머리말",
250
292
  "mergePrompts.reset": "기본값으로 초기화",
@@ -254,7 +296,7 @@ export const MESSAGES = {
254
296
  "composer.changeRequest": "이 줄에 대한 변경 요청하기",
255
297
  "composer.save": "코멘트",
256
298
  "composer.cancel": "취소",
257
- "composer.hint": "Cmd/Ctrl+Enter로 저장, Esc로 취소",
299
+ "composer.hint": "Enter로 저장, Esc로 취소",
258
300
  "composer.delete": "삭제",
259
301
  "comment.kind.q": "❓ 질문",
260
302
  "comment.kind.c": "✎ 변경 요청",
package/dist/preload.cjs CHANGED
@@ -67,6 +67,9 @@ electron_1.contextBridge.exposeInMainWorld("monacoriPty", {
67
67
  write: (msg) => electron_1.ipcRenderer.send("monacori:pty-write", msg),
68
68
  resize: (msg) => electron_1.ipcRenderer.send("monacori:pty-resize", msg),
69
69
  kill: (msg) => electron_1.ipcRenderer.send("monacori:pty-kill", msg),
70
+ // A TUI in the pane rang the terminal bell (e.g. Claude Code finished a turn / needs input). The renderer
71
+ // passes a pre-localized title+body; the main process decides whether to raise a native notification.
72
+ bell: (msg) => electron_1.ipcRenderer.send("monacori:bell", msg),
70
73
  onData: (cb) => {
71
74
  electron_1.ipcRenderer.on("monacori:pty-data", (_event, msg) => cb(msg));
72
75
  },
@@ -74,6 +77,11 @@ electron_1.contextBridge.exposeInMainWorld("monacoriPty", {
74
77
  electron_1.ipcRenderer.on("monacori:pty-exit", (_event, msg) => cb(msg));
75
78
  },
76
79
  });
80
+ // Clipboard bridge — the integrated terminal copies its own selection on Cmd+C (xterm doesn't auto-copy, and
81
+ // the sandboxed renderer's navigator.clipboard is unreliable on file://). Electron's clipboard always works here.
82
+ electron_1.contextBridge.exposeInMainWorld("monacoriClipboard", {
83
+ write: (text) => electron_1.clipboard.writeText(typeof text === "string" ? text : String(text)),
84
+ });
77
85
  // Global settings (locale, …) persisted by the main process under userData so they survive app
78
86
  // restarts — the renderer's file:// localStorage is not reliably persisted across reopens. `all` is
79
87
  // read synchronously at preload so the renderer can pick the locale before first paint; `set` writes
package/dist/render.d.ts CHANGED
@@ -32,6 +32,7 @@ export declare function renderDiffHtml(input: {
32
32
  subtitle: string;
33
33
  projectName: string;
34
34
  projectPath: string;
35
+ branch?: string;
35
36
  watch?: boolean;
36
37
  ignoreWhitespace?: boolean;
37
38
  app?: boolean;
package/dist/render.js CHANGED
@@ -181,6 +181,32 @@ export function renderDiffHtml(input) {
181
181
  const fileNav = renderDiffTree(input.files);
182
182
  const sourceNav = renderSourceTree(input.sourceFiles);
183
183
  const embeddedFiles = input.sourceFiles.filter((file) => file.embedded).length;
184
+ // IntelliJ-style activity rail: an icon per view; click navigates, hover shows a tooltip with the
185
+ // shortcut. data-view drives both the click handler and the active-state highlight (see syncRail).
186
+ const railButton = (view, labelKey, defaultLabel, kbd, svg) => `<button type="button" class="rail-btn" data-view="${view}" data-i18n-aria="${labelKey}" aria-label="${escapeAttr(defaultLabel)}">` +
187
+ `<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${svg}</svg>` +
188
+ `<span class="rail-tip"><span data-i18n="${labelKey}">${escapeHtml(defaultLabel)}</span><kbd>${escapeHtml(kbd)}</kbd></span>` +
189
+ "</button>";
190
+ const activityRail = [
191
+ '<nav class="activity-rail" aria-label="Views">',
192
+ '<div class="rail-group">',
193
+ railButton("changes", "tab.changes", "Changes", "⌘0", '<circle cx="12" cy="12" r="3.2"/><line x1="3.5" y1="12" x2="8.8" y2="12"/><line x1="15.2" y1="12" x2="20.5" y2="12"/>'),
194
+ railButton("files", "tab.files", "Files", "⌘1", '<path d="M4 7.5C4 6.7 4.7 6 5.5 6h3.2c.5 0 .9.2 1.2.6L11 8h7.3c.8 0 1.5.7 1.5 1.5v8c0 .8-.7 1.5-1.5 1.5h-13C4.7 19 4 18.3 4 17.5z"/>'),
195
+ railButton("q", "rail.questions", "Questions", "⌘⇧/", '<path d="M5.5 5.5h13c.8 0 1.5.7 1.5 1.5v6.4c0 .8-.7 1.5-1.5 1.5H12l-4.5 3.6V16.4H5.5c-.8 0-1.5-.7-1.5-1.5V7c0-.8.7-1.5 1.5-1.5z"/><text x="12" y="13" text-anchor="middle" font-size="9.5" font-weight="700" fill="currentColor" stroke="none">?</text>'),
196
+ railButton("c", "rail.changeRequests", "Change requests", "⌘⇧.", '<path d="M14.5 5.5l4 4"/><path d="M4.5 19.5l1-4 10-10 3 3-10 10z"/>'),
197
+ railButton("memo", "memo.title", "Prompt memo", "⌘⇧N", '<rect x="5.5" y="4" width="13" height="16" rx="1.5"/><line x1="8.5" y1="9" x2="15.5" y2="9"/><line x1="8.5" y1="12.5" x2="15.5" y2="12.5"/><line x1="8.5" y1="16" x2="12.5" y2="16"/>'),
198
+ "</div>",
199
+ '<div class="rail-group rail-bottom">',
200
+ // Terminal (Electron only; #terminal-toggle stays hidden until a pty exists). Same id → the existing
201
+ // toggle handler + is-active sync in dock-terminal.js bind to it unchanged.
202
+ input.app
203
+ ? '<button type="button" id="terminal-toggle" class="rail-btn terminal-toggle hidden" data-i18n-aria="terminal.title" aria-label="Terminal"><svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7l4 5-4 5"/><path d="M13 17h6"/></svg><span class="rail-tip"><span data-i18n="terminal.title">Terminal</span><kbd>⌃`</kbd></span></button>'
204
+ : "",
205
+ // Settings gear (#app-info-btn) — existing click handler binds by id.
206
+ '<button type="button" id="app-info-btn" class="rail-btn" aria-haspopup="dialog" data-i18n-aria="settings.title" aria-label="Settings"><span class="rail-gear" aria-hidden="true">⚙</span><span class="rail-tip"><span data-i18n="settings.title">Settings</span><kbd>⌘,</kbd></span></button>',
207
+ "</div>",
208
+ "</nav>",
209
+ ].join("");
184
210
  return [
185
211
  "<!doctype html>",
186
212
  '<html lang="en">',
@@ -198,12 +224,13 @@ export function renderDiffHtml(input) {
198
224
  "<body>",
199
225
  // Boot overlay (removed by the renderer once bootstrap has painted) covers the blank gap after loadFile.
200
226
  '<div id="boot-overlay"><div class="boot-spinner"></div><div>monacori</div></div>',
227
+ activityRail,
201
228
  '<aside class="sidebar" aria-label="Review navigation">',
202
229
  '<div class="sidebar-scroll">',
203
- `<div class="sidebar-brand" title="${escapeAttr(input.projectPath)}"><span class="brand-mark">monacori</span><span class="brand-project">${escapeHtml(input.projectName)}</span></div>`,
230
+ `<div class="sidebar-brand" title="${escapeAttr(input.projectPath)}"><span class="brand-mark">monacori</span><span class="brand-project">${escapeHtml(input.projectName)}</span><span class="brand-branch${input.branch ? "" : " hidden"}" data-i18n-title="rail.branch" title="Current branch"><svg class="brand-branch-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6.5" cy="6" r="2.2"/><circle cx="6.5" cy="18" r="2.2"/><circle cx="17.5" cy="8.5" r="2.2"/><path d="M6.5 8.2v7.6"/><path d="M17.5 10.7c0 3.2-2.2 4.4-5.5 4.9"/></svg><span class="brand-branch-name" id="brand-branch-name">${escapeHtml(input.branch || "")}</span></span></div>`,
204
231
  input.lazy
205
- ? '<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>'
206
- : '<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>',
232
+ ? '<div class="tabs"><button type="button" class="tab active" data-tab="changes" data-i18n="tab.changes" data-i18n-title="tab.changes.title" title="Changes (⌘0)">Changes</button><button type="button" class="tab" data-tab="files" data-i18n="tab.files" data-i18n-title="tab.files.title" title="Files (⌘1)">Files</button></div>'
233
+ : '<div class="tabs"><button type="button" class="tab" data-tab="changes" data-i18n="tab.changes" data-i18n-title="tab.changes.title" title="Changes (⌘0)">Changes</button><button type="button" class="tab active" data-tab="files" data-i18n="tab.files" data-i18n-title="tab.files.title" title="Files (⌘1)">Files</button></div>',
207
234
  `<div class="tab-panel${input.lazy ? "" : " hidden"}" id="changes-panel">${fileNav}</div>`,
208
235
  // Big repos: defer the (potentially huge) source tree — ship it as an inert island, materialized on
209
236
  // the first Files-tab open, so it never builds/lays-out at startup. Small repos render it inline.
@@ -211,7 +238,7 @@ export function renderDiffHtml(input) {
211
238
  ? `<div class="tab-panel hidden" id="files-panel"></div><script type="text/html" id="files-tree-html">${sourceNav}</script>`
212
239
  : `<div class="tab-panel" id="files-panel">${sourceNav}</div>`,
213
240
  "</div>",
214
- `<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>`,
241
+ `<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></div>`,
215
242
  '<div id="footer-progress" class="footer-progress hidden" aria-hidden="true"><div class="footer-progress-bar"></div></div>',
216
243
  "</aside>",
217
244
  '<div class="sidebar-resizer" aria-hidden="true"></div>',
@@ -230,7 +257,7 @@ export function renderDiffHtml(input) {
230
257
  '<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>',
231
258
  '<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>',
232
259
  '<button type="button" id="render-toggle" class="plain-button hidden" aria-pressed="false">Raw</button>',
233
- '<button type="button" id="back-to-diff" class="plain-button" data-i18n="btn.diff">Diff</button>',
260
+ '<button type="button" id="back-to-diff" class="plain-button" data-i18n="btn.diff" data-i18n-title="btn.diff.title" title="Back to diff (F7)">Diff</button>',
234
261
  "</div>",
235
262
  '<div id="source-body" class="source-body empty" data-i18n="source.selectFile">Select a file from the Files tab.</div>',
236
263
  "</section>",
@@ -242,7 +269,7 @@ export function renderDiffHtml(input) {
242
269
  : "",
243
270
  '<div id="quick-open" class="quick-open hidden" role="dialog" aria-modal="true" data-i18n-aria="quickopen.aria" aria-label="Quick open">',
244
271
  '<div class="quick-open-panel">',
245
- '<div class="quick-open-title"><span id="quick-open-mode" data-i18n="quickopen.searchFiles">Search files</span></div>',
272
+ '<div class="quick-open-title"><span id="quick-open-mode" data-i18n="quickopen.searchFiles">Search files</span><span id="quick-open-filter" class="quick-open-filter"></span></div>',
246
273
  '<input id="quick-open-input" type="search" autocomplete="off" spellcheck="false" data-i18n-ph="quickopen.searchFiles" placeholder="Search files">',
247
274
  '<div id="quick-open-results" class="quick-open-results"></div>',
248
275
  '<div id="quick-open-preview" class="quick-open-preview"></div>',
@@ -266,49 +293,63 @@ export function renderDiffHtml(input) {
266
293
  '<button type="button" id="settings-language" class="settings-select mc-select" data-i18n-aria="settings.language"></button>',
267
294
  '<label class="settings-label" for="settings-theme" data-i18n="settings.theme">Theme</label>',
268
295
  '<button type="button" id="settings-theme" class="settings-select mc-select" data-i18n-aria="settings.theme"></button>',
296
+ '<label class="settings-check"><input type="checkbox" id="set-bell-notify"><span data-i18n="settings.bellNotify">Notify when a terminal task finishes (bell)</span></label>',
269
297
  '<div class="app-info-keys">' +
270
298
  '<div class="app-info-keys-h" data-i18n="settings.kbd.title">Keyboard shortcuts</div>' +
299
+ '<div class="keys-cat" data-i18n="settings.kbd.cat.app">App</div>' +
300
+ '<div class="keys-grid">' +
301
+ '<kbd>⌘O</kbd><span data-i18n="kbd.openFolder">Open folder</span>' +
302
+ '<kbd>⌘⇧O</kbd><span data-i18n="kbd.openNewWindow">Open in new window</span>' +
303
+ '<kbd>⌘,</kbd><span data-i18n="kbd.openSettings">Settings</span>' +
304
+ '<kbd>Esc</kbd><span data-i18n="kbd.closeDialog">Close dialog / cancel</span>' +
305
+ '</div>' +
271
306
  '<div class="keys-cat" data-i18n="settings.kbd.cat.nav">Navigation</div>' +
272
307
  '<div class="keys-grid">' +
273
308
  '<kbd>F7</kbd><span data-i18n="kbd.nextChange">Next change</span>' +
274
- '<kbd>Shift+F7</kbd><span data-i18n="kbd.prevChange">Previous change</span>' +
275
- '<kbd>Cmd/Ctrl+1 / 0</kbd><span data-i18n="kbd.filesChangesTab">Files / Changes tab</span>' +
309
+ '<kbd>⇧F7</kbd><span data-i18n="kbd.prevChange">Previous change</span>' +
310
+ '<kbd>⌘1 / 0</kbd><span data-i18n="kbd.filesChangesTab">Files / Changes tab</span>' +
276
311
  '<kbd>Tab</kbd><span data-i18n="kbd.sidebarContent">Sidebar &harr; content</span>' +
277
- '<kbd>Shift Shift</kbd><span data-i18n="kbd.findFile">Find file</span>' +
278
- '<kbd>Cmd/Ctrl+Shift+F</kbd><span data-i18n="kbd.findInFiles">Find in files</span>' +
279
- '<kbd>Cmd/Ctrl+E</kbd><span data-i18n="kbd.recentFiles">Recent files</span>' +
280
- '<kbd>Cmd/Ctrl+B</kbd><span data-i18n="kbd.defUsages">Definition / usages</span>' +
281
- '<kbd>Cmd/Ctrl+&darr;</kbd><span data-i18n="kbd.goToDef">Go to definition</span>' +
282
- '<kbd>Cmd/Ctrl+Shift+[ / ]</kbd><span data-i18n="kbd.prevNextTab">Prev / next tab</span>' +
283
- '<kbd>Cmd/Ctrl+[ / ]</kbd><span data-i18n="kbd.cursorBackForward">Cursor back / forward</span>' +
284
- '<kbd>Opt/Alt+&larr;/&rarr;</kbd><span data-i18n="kbd.wordJump">Word jump (vim w)</span>' +
285
- '<kbd>Cmd/Ctrl+&larr;/&rarr;</kbd><span data-i18n="kbd.lineStartEnd">Line start / end</span>' +
286
- '<kbd>Shift+arrows</kbd><span data-i18n="kbd.extendSelection">Extend selection</span>' +
287
- '<kbd>Cmd/Ctrl+W</kbd><span data-i18n="kbd.closeTab">Close tab</span>' +
312
+ '<kbd>⇧ ⇧</kbd><span data-i18n="kbd.findFile">Find file</span>' +
313
+ '<kbd>⌘⇧F</kbd><span data-i18n="kbd.findInFiles">Find in files</span>' +
314
+ '<kbd>⌘E</kbd><span data-i18n="kbd.recentFiles">Recent files</span>' +
315
+ '<kbd>⌘B</kbd><span data-i18n="kbd.defUsages">Definition / usages</span>' +
316
+ '<kbd>⌘&darr;</kbd><span data-i18n="kbd.goToDef">Go to definition</span>' +
317
+ '<kbd>⌘⇧[ / ]</kbd><span data-i18n="kbd.prevNextTab">Prev / next tab</span>' +
318
+ '<kbd>⌘[ / ]</kbd><span data-i18n="kbd.cursorBackForward">Cursor back / forward</span>' +
319
+ '<kbd>⌥&larr;/&rarr;</kbd><span data-i18n="kbd.wordJump">Word jump (vim w)</span>' +
320
+ '<kbd>⌘&larr;/&rarr;</kbd><span data-i18n="kbd.lineStartEnd">Line start / end</span>' +
321
+ '<kbd>⇧&larr;&uarr;&darr;&rarr;</kbd><span data-i18n="kbd.extendSelection">Extend selection</span>' +
322
+ '<kbd>PageUp / PageDown</kbd><span data-i18n="kbd.pageUpDown">Page up / down</span>' +
323
+ '<kbd>⌘Enter / ⌥Enter</kbd><span data-i18n="kbd.runHttp">Run HTTP request (.http)</span>' +
324
+ '<kbd>⌘W</kbd><span data-i18n="kbd.closeTab">Close tab</span>' +
288
325
  '</div>' +
289
326
  '<div class="keys-cat" data-i18n="settings.kbd.cat.review">Review</div>' +
290
327
  '<div class="keys-grid">' +
291
328
  '<kbd>&lt;</kbd><span data-i18n="kbd.toggleViewed">Toggle viewed</span>' +
292
329
  '<kbd>? &nbsp;&gt;</kbd><span data-i18n="kbd.addQuestionChange">Add question / change</span>' +
293
- '<kbd>Cmd/Ctrl+Shift+/ .</kbd><span data-i18n="kbd.allQuestionsChanges">All questions / changes</span>' +
294
- '<kbd>Cmd/Ctrl+Shift+W</kbd><span data-i18n="kbd.ignoreWhitespace">Ignore whitespace</span>' +
295
- '<kbd>Cmd/Ctrl+Enter</kbd><span data-i18n="kbd.saveComment">Save comment</span>' +
296
- '<kbd>Cmd/Ctrl+Shift+N</kbd><span data-i18n="kbd.promptMemo">Prompt memo</span>' +
297
- '<kbd>Cmd/Ctrl+Shift+&#39;</kbd><span data-i18n="kbd.maximizePanel">Maximize panel</span>' +
330
+ '<kbd>⌘⇧/ .</kbd><span data-i18n="kbd.allQuestionsChanges">All questions / changes</span>' +
331
+ '<kbd>⌘⇧W</kbd><span data-i18n="kbd.ignoreWhitespace">Ignore whitespace</span>' +
332
+ '<kbd>⌘Enter</kbd><span data-i18n="kbd.saveComment">Save comment</span>' +
333
+ '<kbd>e</kbd><span data-i18n="kbd.editComment">Edit comment (when selected)</span>' +
334
+ '<kbd>Backspace / Delete</kbd><span data-i18n="kbd.deleteComment">Delete comment (when selected)</span>' +
335
+ '<kbd>⌥&uarr;/&darr;</kbd><span data-i18n="kbd.stepComments">Step between comments (merged)</span>' +
336
+ '<kbd>⌥Enter</kbd><span data-i18n="kbd.mergedSend">Comment menu / send to pane (merged)</span>' +
337
+ '<kbd>⌘⇧N</kbd><span data-i18n="kbd.promptMemo">Prompt memo</span>' +
338
+ '<kbd>⌘⇧&#39;</kbd><span data-i18n="kbd.maximizePanel">Maximize panel</span>' +
298
339
  '</div>' +
299
340
  '<div class="keys-cat" data-i18n="settings.kbd.cat.terminal">Terminal</div>' +
300
341
  '<div class="keys-grid">' +
301
- '<kbd>Ctrl+`</kbd><span data-i18n="kbd.toggleTerminal">Toggle terminal</span>' +
302
- '<kbd>Cmd/Ctrl+D</kbd><span data-i18n="kbd.splitPane">Split pane</span>' +
303
- '<kbd>Cmd/Ctrl+Alt+[ / ]</kbd><span data-i18n="kbd.focusPane">Focus prev / next pane</span>' +
304
- '<kbd>Cmd/Ctrl+Alt+R</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
305
- '<kbd>Cmd/Ctrl+W</kbd><span data-i18n="kbd.closeTerminal">Close terminal (when focused)</span>' +
342
+ '<kbd>⌃` / ⌥F12</kbd><span data-i18n="kbd.toggleTerminal">Toggle terminal</span>' +
343
+ '<kbd>⌘D</kbd><span data-i18n="kbd.splitPane">Split pane</span>' +
344
+ '<kbd>⌘⌥[ / ]</kbd><span data-i18n="kbd.focusPane">Focus prev / next pane</span>' +
345
+ '<kbd>⌘⌥R</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
346
+ '<kbd>⌘W</kbd><span data-i18n="kbd.closeTerminal">Close terminal (when focused)</span>' +
306
347
  '</div>' +
307
348
  '</div>',
308
349
  "</section>",
309
350
  '<section class="settings-section hidden" data-cat="prompts">',
310
351
  '<div class="settings-h" data-i18n="mergePrompts.title">Merge prompts</div>',
311
- '<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>',
352
+ '<div class="settings-desc" data-i18n="mergePrompts.desc">Heading prepended to the merged prompt opened with ⌘⇧/ (questions) and ⌘⇧. (change requests). Leave blank to use the default.</div>',
312
353
  '<label class="settings-label" for="settings-prompt-q" data-i18n="mergePrompts.qHeading">Questions heading</label>',
313
354
  '<textarea id="settings-prompt-q" class="settings-textarea" rows="4" spellcheck="false"></textarea>',
314
355
  '<label class="settings-label" for="settings-prompt-c" data-i18n="mergePrompts.cHeading">Change-requests heading</label>',
package/dist/util.d.ts CHANGED
@@ -19,3 +19,8 @@ export declare function listRecentFiles(dir: string, limit: number): string[];
19
19
  export declare function sanitizeTerminalEnv(env: NodeJS.ProcessEnv): {
20
20
  [key: string]: string;
21
21
  };
22
+ export declare function ensureUtf8Locale(env: {
23
+ [key: string]: string;
24
+ }): {
25
+ [key: string]: string;
26
+ };
package/dist/util.js CHANGED
@@ -170,3 +170,24 @@ export function sanitizeTerminalEnv(env) {
170
170
  }
171
171
  return out;
172
172
  }
173
+ // GUI launches (Finder double-click, Spotlight, the `mo` relauncher) often start with no LANG/LC_* at
174
+ // all, so the pty's shell — and tools it runs, notably git's `less` pager — fall back to the C locale and
175
+ // render UTF-8 text (e.g. Korean commit messages) as escaped bytes like "<EA><B5><AD>". Force a UTF-8
176
+ // codeset unless the inherited locale already is one. Mutates and returns the given object.
177
+ export function ensureUtf8Locale(env) {
178
+ const isUtf8 = (value) => !!value && /utf-?8/i.test(value);
179
+ // LC_ALL overrides LANG and every LC_* category; a non-UTF-8 LC_ALL (e.g. "C") would defeat the LANG we
180
+ // set below, so drop it and let LANG win.
181
+ if (env.LC_ALL && !isUtf8(env.LC_ALL))
182
+ delete env.LC_ALL;
183
+ if (isUtf8(env.LC_ALL) || isUtf8(env.LC_CTYPE) || isUtf8(env.LANG))
184
+ return env;
185
+ // Same reasoning for a stray non-UTF-8 LC_CTYPE — it overrides LANG for character handling.
186
+ if (env.LC_CTYPE && !isUtf8(env.LC_CTYPE))
187
+ delete env.LC_CTYPE;
188
+ // Preserve the user's region when LANG names a real locale (ko_KR -> ko_KR.UTF-8); otherwise (C/POSIX/
189
+ // empty) fall back to en_US.UTF-8, which always exists on macOS.
190
+ const base = env.LANG && /^[A-Za-z]{2}_[A-Za-z]{2}/.test(env.LANG) ? env.LANG.split(".")[0] : "en_US";
191
+ env.LANG = base + ".UTF-8";
192
+ return env;
193
+ }