diff --git a/codec_dashboard.html b/codec_dashboard.html
index 11771680..3ef923ae 100644
--- a/codec_dashboard.html
+++ b/codec_dashboard.html
@@ -165,7 +165,7 @@
.tab-badge.hidden { display: none; }
/* ── Content ── */
-.content { flex: 1; min-height: 0; overflow-y: auto; padding-bottom: calc(var(--input-h,76px) + 16px); -webkit-overflow-scrolling: touch; }
+.content { flex: 1; min-height: 0; overflow-y: auto; padding-bottom: 16px; -webkit-overflow-scrolling: touch; }
.panel { display: none; padding: 14px; }
.panel.active { display: block; }
#panel-chat { position: relative; }
@@ -179,7 +179,13 @@
/* ── Input Area (matches Deep Chat) ── */
.input-area {
- position: fixed; left: 0; right: 0; bottom: 0; z-index: 9997;
+ /* Desktop/native app: a normal flex child at the bottom of .app, so the
+ scroll area (.content) sizes to the space left over and a long reply
+ scrolls fully into view. It only goes position:fixed on phones (see the
+ touch media query) where the docked nav + gesture bars need it.
+ Regression note: 1612bf0 made it fixed on EVERY size, which hid the last
+ ~60px of every reply behind the composer in the desktop window. */
+ flex-shrink: 0;
padding: 8px 12px; border-top: 1px solid var(--border); background: var(--surface);
padding-bottom: calc(8px + var(--safe-bottom));
}
@@ -614,7 +620,7 @@
.text-size-group button.sel{background:var(--accent);border-color:var(--accent);color:#fff}
@media (max-width: 768px), (max-height: 600px) and (pointer: coarse){
- .input-area{bottom:var(--nav-h,56px);padding-bottom:8px}
+ .input-area{position:fixed;left:0;right:0;bottom:var(--nav-h,56px);z-index:9997;padding-bottom:8px}
.page-nav{padding-bottom:calc(6px + env(safe-area-inset-bottom,0px))}
.content{padding-bottom:calc(var(--nav-h,56px) + var(--input-h,64px) + 12px)}
}
diff --git a/packaging/macos/launcher/codec_launcher.swift b/packaging/macos/launcher/codec_launcher.swift
index f2543baa..a32b617b 100644
--- a/packaging/macos/launcher/codec_launcher.swift
+++ b/packaging/macos/launcher/codec_launcher.swift
@@ -140,6 +140,13 @@ final class Launcher: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
log("CODEC launched from \(bundleURL.path)")
+ // Without a main menu carrying an Edit menu, macOS has nowhere to
+ // dispatch Cmd-C/X/V/A/Z, so paste (etc.) silently failed inside the
+ // dashboard's WKWebView — the shortcuts only worked in a real browser
+ // because the browser ships its own Edit menu. The status-bar item's
+ // menu is NOT the main menu and does not supply these key-equivalents.
+ installMainMenu()
+
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = item.button {
button.image = NSImage(systemSymbolName: "waveform", accessibilityDescription: "CODEC")
@@ -161,6 +168,46 @@ final class Launcher: NSObject, NSApplicationDelegate {
}
}
+ /// Install the app's main menu. The only functional requirement is a
+ /// standard Edit menu — its items use the first-responder selectors
+ /// (`paste:`, `copy:`, ...) with a nil target, so the keystroke walks the
+ /// responder chain to the focused WKWebView and the clipboard works inside
+ /// the app window exactly as it does in a browser.
+ private func installMainMenu() {
+ let mainMenu = NSMenu()
+
+ // App menu. macOS always treats the first submenu as the app menu; it
+ // owns Cmd-Q, Cmd-H and friends.
+ let appItem = NSMenuItem()
+ mainMenu.addItem(appItem)
+ let appMenu = NSMenu()
+ appItem.submenu = appMenu
+ appMenu.addItem(withTitle: "Hide CODEC", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
+ let hideOthers = appMenu.addItem(withTitle: "Hide Others",
+ action: #selector(NSApplication.hideOtherApplications(_:)),
+ keyEquivalent: "h")
+ hideOthers.keyEquivalentModifierMask = [.command, .option]
+ appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
+ appMenu.addItem(.separator())
+ appMenu.addItem(withTitle: "Quit CODEC", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
+
+ // Edit menu — the reason this method exists.
+ let editItem = NSMenuItem()
+ mainMenu.addItem(editItem)
+ let editMenu = NSMenu(title: "Edit")
+ editItem.submenu = editMenu
+ editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
+ let redo = editMenu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "z")
+ redo.keyEquivalentModifierMask = [.command, .shift]
+ editMenu.addItem(.separator())
+ editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
+ editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
+ editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
+ editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
+
+ NSApp.mainMenu = mainMenu
+ }
+
private func rebuildMenu(status: String, enabled: Bool) {
let menu = NSMenu()
menu.addItem(withTitle: status, action: nil, keyEquivalent: "")