chore: sync repo with deployed state — clean up old specs/plans, desktop tweaks
This commit is contained in:
@@ -11,14 +11,18 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
# macOS — universal build (arm64 + x64) on Apple Silicon runner
|
||||||
platform: windows
|
|
||||||
- os: ubuntu-latest
|
|
||||||
platform: linux
|
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
platform: mac
|
args: --mac --arm64 --x64
|
||||||
|
# Windows — x64 + arm64 on x64 runner
|
||||||
|
- os: windows-latest
|
||||||
|
args: --win --x64 --arm64
|
||||||
|
# Linux — x64 on x64 runner
|
||||||
|
- os: ubuntu-latest
|
||||||
|
args: --linux --x64
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
@@ -26,10 +30,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Linux build dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libx11-dev libxtst-dev libxt-dev libxkbcommon-dev
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -43,8 +53,12 @@ jobs:
|
|||||||
- name: Build shared package
|
- name: Build shared package
|
||||||
run: pnpm --filter @backspace/shared build
|
run: pnpm --filter @backspace/shared build
|
||||||
|
|
||||||
- name: Build desktop app
|
- name: Compile desktop TypeScript
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop
|
||||||
run: pnpm run build
|
run: pnpm exec tsc
|
||||||
|
|
||||||
|
- name: Build and publish desktop app
|
||||||
|
working-directory: packages/desktop
|
||||||
|
run: pnpm exec electron-builder ${{ matrix.args }} --publish always
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -6,8 +6,18 @@ directories:
|
|||||||
files:
|
files:
|
||||||
- dist/**/*
|
- dist/**/*
|
||||||
- resources/**/*
|
- resources/**/*
|
||||||
|
# Exclude host-compiled native module artifacts from the asar.
|
||||||
|
# electron-rebuild (postinstall) compiles uiohook-napi for the build machine,
|
||||||
|
# creating build/Release/*.node. node-gyp-build checks build/ BEFORE prebuilds/,
|
||||||
|
# so if these directories enter the asar, the app crashes on every platform
|
||||||
|
# (including the build platform after afterPack removes the unpacked file).
|
||||||
|
- "!**/node_modules/uiohook-napi/build/**"
|
||||||
|
- "!**/node_modules/uiohook-napi/build.bak/**"
|
||||||
|
- "!**/node_modules/uiohook-napi/bin/**"
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- "**/*.node"
|
- "**/*.node"
|
||||||
|
npmRebuild: false
|
||||||
|
afterPack: ./scripts/afterPack.js
|
||||||
publish:
|
publish:
|
||||||
- provider: github
|
- provider: github
|
||||||
owner: TheZwiss
|
owner: TheZwiss
|
||||||
@@ -25,7 +35,7 @@ mac:
|
|||||||
- dmg
|
- dmg
|
||||||
- zip
|
- zip
|
||||||
win:
|
win:
|
||||||
icon: ./build/icon.ico
|
icon: ./build/icon.png
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
linux:
|
linux:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dev": "mkdir -p build && cp ../../icon.png build/icon.png && bash scripts/gen-icns.sh && cp build/icon.icns $(find ../../node_modules -path '*/electron/dist/Electron.app/Contents/Resources/electron.icns' 2>/dev/null | head -1) 2>/dev/null; tsc && electron .",
|
"dev": "mkdir -p build && cp ../../icon.png build/icon.png && bash scripts/gen-icns.sh && cp build/icon.icns $(find ../../node_modules -path '*/electron/dist/Electron.app/Contents/Resources/electron.icns' 2>/dev/null | head -1) 2>/dev/null; tsc && electron .",
|
||||||
"prebuild": "mkdir -p build && cp ../../icon.png build/icon.png",
|
"prebuild": "mkdir -p build && cp ../../icon.png build/icon.png",
|
||||||
"build": "tsc && electron-builder",
|
"build": "tsc && electron-builder",
|
||||||
|
"build:all": "tsc && electron-builder --mac --win --linux --arm64 --x64",
|
||||||
"clean": "rm -rf dist dist-electron",
|
"clean": "rm -rf dist dist-electron",
|
||||||
"postinstall": "electron-rebuild -f -w uiohook-napi"
|
"postinstall": "electron-rebuild -f -w uiohook-napi"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// afterPack hook for electron-builder
|
||||||
|
// Removes host-compiled native module artifacts so cross-platform builds
|
||||||
|
// use the correct prebuilt binaries from the `prebuilds/` directory.
|
||||||
|
//
|
||||||
|
// Why this is needed:
|
||||||
|
// `electron-rebuild` (postinstall) compiles uiohook-napi for the BUILD
|
||||||
|
// machine (e.g. macOS arm64), placing the binary in `build/Release/`.
|
||||||
|
// `node-gyp-build` checks `build/Release/` BEFORE `prebuilds/{platform}/`,
|
||||||
|
// so Windows/Linux packages would load the macOS binary and crash.
|
||||||
|
//
|
||||||
|
// What this does:
|
||||||
|
// 1. Removes `build/`, `build.bak/`, `bin/` dirs (host-compiled artifacts)
|
||||||
|
// 2. Strips prebuilts for platforms other than the target
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
exports.default = async function afterPack(context) {
|
||||||
|
const platform = context.electronPlatformName; // 'darwin', 'linux', 'win32'
|
||||||
|
const appDir = path.join(
|
||||||
|
context.appOutDir,
|
||||||
|
// macOS bundles resources inside the .app
|
||||||
|
platform === 'darwin'
|
||||||
|
? `${context.packager.appInfo.productFilename}.app/Contents/Resources`
|
||||||
|
: 'resources'
|
||||||
|
);
|
||||||
|
|
||||||
|
const asarUnpacked = path.join(appDir, 'app.asar.unpacked');
|
||||||
|
const uiohookDir = path.join(asarUnpacked, 'node_modules', 'uiohook-napi');
|
||||||
|
|
||||||
|
if (!fs.existsSync(uiohookDir)) {
|
||||||
|
console.log(`[afterPack] uiohook-napi not found in ${platform} build — skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Remove host-compiled artifacts that shadow prebuilts
|
||||||
|
for (const dir of ['build', 'build.bak', 'bin']) {
|
||||||
|
const target = path.join(uiohookDir, dir);
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
fs.rmSync(target, { recursive: true, force: true });
|
||||||
|
console.log(`[afterPack] Removed ${dir}/ from uiohook-napi (${platform})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Strip prebuilts for other platforms (saves ~1-2MB per build)
|
||||||
|
const prebuildsDir = path.join(uiohookDir, 'prebuilds');
|
||||||
|
if (fs.existsSync(prebuildsDir)) {
|
||||||
|
for (const entry of fs.readdirSync(prebuildsDir)) {
|
||||||
|
const entryPlatform = entry.split('-')[0]; // 'darwin', 'linux', 'win32'
|
||||||
|
if (entryPlatform !== platform) {
|
||||||
|
fs.rmSync(path.join(prebuildsDir, entry), { recursive: true, force: true });
|
||||||
|
console.log(`[afterPack] Stripped prebuilds/${entry} (not needed for ${platform})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[afterPack] Native module cleanup done for ${platform}`);
|
||||||
|
};
|
||||||
@@ -55,6 +55,8 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess
|
|||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [isNearBottom, setIsNearBottom] = useState(true);
|
const [isNearBottom, setIsNearBottom] = useState(true);
|
||||||
const isNearBottomRef = useRef(true);
|
const isNearBottomRef = useRef(true);
|
||||||
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
|
const isAtBottomRef = useRef(true);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const showInitialSkeleton = useDelayedLoading(isLoading && messages.length === 0);
|
const showInitialSkeleton = useDelayedLoading(isLoading && messages.length === 0);
|
||||||
const showPaginationSkeleton = useDelayedLoading(isLoadingMore);
|
const showPaginationSkeleton = useDelayedLoading(isLoadingMore);
|
||||||
@@ -110,11 +112,13 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess
|
|||||||
|
|
||||||
prevMessagesLength.current = 0;
|
prevMessagesLength.current = 0;
|
||||||
|
|
||||||
// If we have a saved position for the incoming channel, don't mark as near-bottom
|
// If we have a saved position for the incoming channel, don't mark as near/at-bottom
|
||||||
// — this prevents the ResizeObserver from snapping to bottom before the restore rAF fires
|
// — this prevents the ResizeObserver from snapping to bottom before the restore rAF fires
|
||||||
const willRestore = useChatStore.getState().scrollPositions.has(channelId);
|
const willRestore = useChatStore.getState().scrollPositions.has(channelId);
|
||||||
setIsNearBottom(!willRestore);
|
setIsNearBottom(!willRestore);
|
||||||
isNearBottomRef.current = !willRestore;
|
isNearBottomRef.current = !willRestore;
|
||||||
|
setIsAtBottom(!willRestore);
|
||||||
|
isAtBottomRef.current = !willRestore;
|
||||||
}, [channelId, saveScrollPosition]);
|
}, [channelId, saveScrollPosition]);
|
||||||
|
|
||||||
// Handle scrolling: initial load restores position or snaps to bottom,
|
// Handle scrolling: initial load restores position or snaps to bottom,
|
||||||
@@ -139,17 +143,20 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess
|
|||||||
const near = dist < 5000;
|
const near = dist < 5000;
|
||||||
setIsNearBottom(near);
|
setIsNearBottom(near);
|
||||||
isNearBottomRef.current = near;
|
isNearBottomRef.current = near;
|
||||||
|
const atBot = dist < 150;
|
||||||
|
setIsAtBottom(atBot);
|
||||||
|
isAtBottomRef.current = atBot;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No saved anchor or message not in cache — snap to bottom
|
// No saved anchor or message not in cache — snap to bottom
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
});
|
});
|
||||||
} else if (messages.length > prev && isNearBottom) {
|
} else if (messages.length > prev && isAtBottom) {
|
||||||
// New messages arrived while near bottom — smooth scroll
|
// New messages arrived while at bottom — smooth scroll
|
||||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}, [messages.length, isNearBottom, channelId]);
|
}, [messages.length, isAtBottom, channelId]);
|
||||||
|
|
||||||
// Auto-scroll when content height grows (embeds/images loading) while near bottom
|
// Auto-scroll when content height grows (embeds/images loading) while near bottom
|
||||||
const hasMessages = messages.length > 0;
|
const hasMessages = messages.length > 0;
|
||||||
@@ -227,8 +234,13 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess
|
|||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
// Check if near bottom
|
// Check scroll position relative to bottom
|
||||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
|
// "at bottom" = within 150px — used for auto-scrolling on new messages
|
||||||
|
const atBottom = distanceFromBottom < 150;
|
||||||
|
setIsAtBottom(atBottom);
|
||||||
|
isAtBottomRef.current = atBottom;
|
||||||
|
// "near bottom" = within 5000px — used for "Jump to Present" button visibility
|
||||||
const nearBottom = distanceFromBottom < 5000;
|
const nearBottom = distanceFromBottom < 5000;
|
||||||
setIsNearBottom(nearBottom);
|
setIsNearBottom(nearBottom);
|
||||||
isNearBottomRef.current = nearBottom;
|
isNearBottomRef.current = nearBottom;
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ export function UpdateToast() {
|
|||||||
<div className="fixed bottom-6 left-6 z-[300] animate-slide-up">
|
<div className="fixed bottom-6 left-6 z-[300] animate-slide-up">
|
||||||
<div className="glass-pill rounded-xl px-4 py-3 flex items-center gap-3 max-w-[380px]">
|
<div className="glass-pill rounded-xl px-4 py-3 flex items-center gap-3 max-w-[380px]">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-txt-primary">Update available</p>
|
<p className="text-sm font-medium text-txt-primary">Update failed</p>
|
||||||
<p className="text-xs text-txt-secondary truncate">
|
<p className="text-xs text-txt-secondary truncate">
|
||||||
Auto-install failed — download manually
|
Auto-update failed — download manually
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
|
|||||||
Reference in New Issue
Block a user