@happy-nut/monacori 0.1.21 → 0.1.23

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/render.js CHANGED
@@ -181,6 +181,36 @@ 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
+ // History (Cmd+9): Electron only — the git-log bridge (window.monacoriGit) is exposed there.
201
+ input.app
202
+ ? railButton("history", "rail.history", "History", "⌘9", '<circle cx="12" cy="12" r="8.3"/><path d="M12 7.4v5l3.2 1.9"/>')
203
+ : "",
204
+ // Terminal (Electron only; #terminal-toggle stays hidden until a pty exists). Same id → the existing
205
+ // toggle handler + is-active sync in dock-terminal.js bind to it unchanged.
206
+ input.app
207
+ ? '<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>'
208
+ : "",
209
+ // Settings gear (#app-info-btn) — existing click handler binds by id.
210
+ '<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>',
211
+ "</div>",
212
+ "</nav>",
213
+ ].join("");
184
214
  return [
185
215
  "<!doctype html>",
186
216
  '<html lang="en">',
@@ -198,12 +228,13 @@ export function renderDiffHtml(input) {
198
228
  "<body>",
199
229
  // Boot overlay (removed by the renderer once bootstrap has painted) covers the blank gap after loadFile.
200
230
  '<div id="boot-overlay"><div class="boot-spinner"></div><div>monacori</div></div>',
231
+ activityRail,
201
232
  '<aside class="sidebar" aria-label="Review navigation">',
202
233
  '<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>`,
234
+ `<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
235
  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>',
236
+ ? '<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>'
237
+ : '<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
238
  `<div class="tab-panel${input.lazy ? "" : " hidden"}" id="changes-panel">${fileNav}</div>`,
208
239
  // Big repos: defer the (potentially huge) source tree — ship it as an inert island, materialized on
209
240
  // the first Files-tab open, so it never builds/lays-out at startup. Small repos render it inline.
@@ -211,7 +242,7 @@ export function renderDiffHtml(input) {
211
242
  ? `<div class="tab-panel hidden" id="files-panel"></div><script type="text/html" id="files-tree-html">${sourceNav}</script>`
212
243
  : `<div class="tab-panel" id="files-panel">${sourceNav}</div>`,
213
244
  "</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>`,
245
+ `<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
246
  '<div id="footer-progress" class="footer-progress hidden" aria-hidden="true"><div class="footer-progress-bar"></div></div>',
216
247
  "</aside>",
217
248
  '<div class="sidebar-resizer" aria-hidden="true"></div>',
@@ -230,7 +261,7 @@ export function renderDiffHtml(input) {
230
261
  '<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
262
  '<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
263
  '<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>',
264
+ '<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
265
  "</div>",
235
266
  '<div id="source-body" class="source-body empty" data-i18n="source.selectFile">Select a file from the Files tab.</div>',
236
267
  "</section>",
@@ -242,7 +273,7 @@ export function renderDiffHtml(input) {
242
273
  : "",
243
274
  '<div id="quick-open" class="quick-open hidden" role="dialog" aria-modal="true" data-i18n-aria="quickopen.aria" aria-label="Quick open">',
244
275
  '<div class="quick-open-panel">',
245
- '<div class="quick-open-title"><span id="quick-open-mode" data-i18n="quickopen.searchFiles">Search files</span></div>',
276
+ '<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
277
  '<input id="quick-open-input" type="search" autocomplete="off" spellcheck="false" data-i18n-ph="quickopen.searchFiles" placeholder="Search files">',
247
278
  '<div id="quick-open-results" class="quick-open-results"></div>',
248
279
  '<div id="quick-open-preview" class="quick-open-preview"></div>',
@@ -266,49 +297,66 @@ export function renderDiffHtml(input) {
266
297
  '<button type="button" id="settings-language" class="settings-select mc-select" data-i18n-aria="settings.language"></button>',
267
298
  '<label class="settings-label" for="settings-theme" data-i18n="settings.theme">Theme</label>',
268
299
  '<button type="button" id="settings-theme" class="settings-select mc-select" data-i18n-aria="settings.theme"></button>',
300
+ '<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
301
  '<div class="app-info-keys">' +
270
302
  '<div class="app-info-keys-h" data-i18n="settings.kbd.title">Keyboard shortcuts</div>' +
303
+ '<div class="keys-cat" data-i18n="settings.kbd.cat.app">App</div>' +
304
+ '<div class="keys-grid">' +
305
+ '<kbd>⌘O</kbd><span data-i18n="kbd.openFolder">Open folder</span>' +
306
+ '<kbd>⌘⇧O</kbd><span data-i18n="kbd.openNewWindow">Open in new window</span>' +
307
+ '<kbd>⌘,</kbd><span data-i18n="kbd.openSettings">Settings</span>' +
308
+ '<kbd>⌘L</kbd><span data-i18n="kbd.gotoLine">Go to line</span>' +
309
+ '<kbd>⌘K</kbd><span data-i18n="kbd.copyLocation">Copy file:line</span>' +
310
+ '<kbd>⌥Enter</kbd><span data-i18n="kbd.rowActions">Sidebar file actions (path / Finder / terminal)</span>' +
311
+ '<kbd>Esc</kbd><span data-i18n="kbd.closeDialog">Close dialog / cancel</span>' +
312
+ '</div>' +
271
313
  '<div class="keys-cat" data-i18n="settings.kbd.cat.nav">Navigation</div>' +
272
314
  '<div class="keys-grid">' +
273
315
  '<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>' +
316
+ '<kbd>⇧F7</kbd><span data-i18n="kbd.prevChange">Previous change</span>' +
317
+ '<kbd>⌘1 / 0</kbd><span data-i18n="kbd.filesChangesTab">Files / Changes tab</span>' +
276
318
  '<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>' +
319
+ '<kbd>⇧ ⇧</kbd><span data-i18n="kbd.findFile">Find file</span>' +
320
+ '<kbd>⌘⇧F</kbd><span data-i18n="kbd.findInFiles">Find in files</span>' +
321
+ '<kbd>⌘E</kbd><span data-i18n="kbd.recentFiles">Recent files</span>' +
322
+ '<kbd>⌘B</kbd><span data-i18n="kbd.defUsages">Definition / usages</span>' +
323
+ '<kbd>⌘&darr;</kbd><span data-i18n="kbd.goToDef">Go to definition</span>' +
324
+ '<kbd>⌘⇧[ / ]</kbd><span data-i18n="kbd.prevNextTab">Prev / next tab</span>' +
325
+ '<kbd>⌘[ / ]</kbd><span data-i18n="kbd.cursorBackForward">Cursor back / forward</span>' +
326
+ '<kbd>⌥&larr;/&rarr;</kbd><span data-i18n="kbd.wordJump">Word jump (vim w)</span>' +
327
+ '<kbd>⌘&larr;/&rarr;</kbd><span data-i18n="kbd.lineStartEnd">Line start / end</span>' +
328
+ '<kbd>⇧&larr;&uarr;&darr;&rarr;</kbd><span data-i18n="kbd.extendSelection">Extend selection</span>' +
329
+ '<kbd>PageUp / PageDown</kbd><span data-i18n="kbd.pageUpDown">Page up / down</span>' +
330
+ '<kbd>⌘Enter / ⌥Enter</kbd><span data-i18n="kbd.runHttp">Run HTTP request (.http)</span>' +
331
+ '<kbd>⌘W</kbd><span data-i18n="kbd.closeTab">Close tab</span>' +
288
332
  '</div>' +
289
333
  '<div class="keys-cat" data-i18n="settings.kbd.cat.review">Review</div>' +
290
334
  '<div class="keys-grid">' +
291
335
  '<kbd>&lt;</kbd><span data-i18n="kbd.toggleViewed">Toggle viewed</span>' +
292
336
  '<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>' +
337
+ '<kbd>⌘⇧/ .</kbd><span data-i18n="kbd.allQuestionsChanges">All questions / changes</span>' +
338
+ '<kbd>⌘⇧W</kbd><span data-i18n="kbd.ignoreWhitespace">Ignore whitespace</span>' +
339
+ '<kbd>⌘Enter</kbd><span data-i18n="kbd.saveComment">Save comment</span>' +
340
+ '<kbd>e</kbd><span data-i18n="kbd.editComment">Edit comment (when selected)</span>' +
341
+ '<kbd>Backspace / Delete</kbd><span data-i18n="kbd.deleteComment">Delete comment (when selected)</span>' +
342
+ '<kbd>⌥&uarr;/&darr;</kbd><span data-i18n="kbd.stepComments">Step between comments (merged)</span>' +
343
+ '<kbd>⌥Enter</kbd><span data-i18n="kbd.mergedSend">Comment menu / send to pane (merged)</span>' +
344
+ '<kbd>⌘⇧N</kbd><span data-i18n="kbd.promptMemo">Prompt memo</span>' +
345
+ '<kbd>⌘⇧&#39;</kbd><span data-i18n="kbd.maximizePanel">Maximize panel</span>' +
298
346
  '</div>' +
299
347
  '<div class="keys-cat" data-i18n="settings.kbd.cat.terminal">Terminal</div>' +
300
348
  '<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>' +
349
+ '<kbd>⌃` / ⌥F12</kbd><span data-i18n="kbd.toggleTerminal">Toggle terminal</span>' +
350
+ '<kbd>⌘D</kbd><span data-i18n="kbd.splitPane">Split pane</span>' +
351
+ '<kbd>⌘⌥[ / ]</kbd><span data-i18n="kbd.focusPane">Focus prev / next pane</span>' +
352
+ '<kbd>⌘⌥R</kbd><span data-i18n="kbd.renamePane">Rename pane</span>' +
353
+ '<kbd>⌘W</kbd><span data-i18n="kbd.closeTerminal">Close terminal (when focused)</span>' +
306
354
  '</div>' +
307
355
  '</div>',
308
356
  "</section>",
309
357
  '<section class="settings-section hidden" data-cat="prompts">',
310
358
  '<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>',
359
+ '<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
360
  '<label class="settings-label" for="settings-prompt-q" data-i18n="mergePrompts.qHeading">Questions heading</label>',
313
361
  '<textarea id="settings-prompt-q" class="settings-textarea" rows="4" spellcheck="false"></textarea>',
314
362
  '<label class="settings-label" for="settings-prompt-c" data-i18n="mergePrompts.cHeading">Change-requests heading</label>',
@@ -318,6 +366,19 @@ export function renderDiffHtml(input) {
318
366
  "</div>",
319
367
  "</div>",
320
368
  "</div>",
369
+ // Git history (Cmd+9): full-screen overlay — commit list (with graph lanes) on the left, the selected
370
+ // commit's message + diff on the right. Populated lazily by the renderer from window.monacoriGit.
371
+ '<div id="history-view" class="history-view hidden" role="dialog" aria-modal="true" data-i18n-aria="history.title" aria-label="Git history">',
372
+ '<div class="history-bar">',
373
+ '<span class="history-title" data-i18n="history.title">History</span>',
374
+ '<input id="history-search" type="search" class="history-search" autocomplete="off" spellcheck="false" data-i18n-ph="history.search" placeholder="Filter by message or author">',
375
+ '<button type="button" id="history-close" class="dock-btn" data-i18n-title="history.close" title="Close" aria-label="Close">&times;</button>',
376
+ "</div>",
377
+ '<div class="history-body">',
378
+ '<div id="history-list" class="history-list"></div>',
379
+ '<div id="history-detail" class="history-detail"></div>',
380
+ "</div>",
381
+ "</div>",
321
382
  input.diffIslands || "",
322
383
  `<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>`,
323
384
  `<script type="application/json" id="i18n-data">${jsonForScript(MESSAGES)}</script>`,
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
+ }