feat: scaffold Goose2 desktop app with full UI and testing infrastructure (#2)
* feat: scaffold Tauri + React app with window dragging and app icon Sets up the goose2 desktop app with: - Tauri 2 + React 19 + Vite + Tailwind CSS - Custom titlebar with drag-to-move and double-click maximize/restore - Window state persistence via tauri-plugin-window-state - App icon ported from goose-desktop (cyan/amber yin-yang design) - Theme system with dark/light/system modes - Hermit toolchain (node, pnpm, biome, just, lefthook) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add app shell layout with tabs, sidebar, and status bar Adds the main application layout ported from goose2.0's visual design: - TabBar with session tabs, home button, and sidebar toggle - Collapsible sidebar with smooth width transition - Status bar with model name, token count, and status indicator - AppShell component wiring everything together Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add settings modal with theme section + quality fixes Settings: - Settings modal with sidebar nav (Appearance, General, About) - Theme selector (Light/Dark/System) wired to ThemeProvider - Accent color picker and density selector (visual placeholders) - Open via Cmd+, or Settings button in sidebar Quality fixes: - Fix stale state bug in tab close handler - Fix unreachable useTheme guard, add OS theme change listener - Improve accessibility (ARIA roles, labels, semantic elements) - Add prefers-reduced-motion support - Extract shared Tab type to features/tabs/types.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove references to nonexistent theme-tokens module The previous commit introduced imports for a theme-tokens module that doesn't exist. Remove those imports and define the needed types locally in ThemeProvider. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: wire up accent colors and density, add AGENTS.md Theming: - ThemeProvider now manages accent color and density (persisted to localStorage) - Accent color applied via --color-accent CSS variable on :root - Density applied via --density-spacing CSS variable (multiplier) - AppearanceSettings uses ThemeProvider context instead of local state - Tailwind config maps accent color variable - Settings modal animations enhanced to match goose2.0 reference - Component classes updated to use semantic token names Documentation: - AGENTS.md captures all project conventions, patterns, and decisions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace ambiguous Tailwind arbitrary durations with config values Add duration-400 and duration-600 to Tailwind config to avoid ambiguous class warnings from arbitrary value syntax. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: sidebar minimizes to icon strip instead of hiding The sidebar now shrinks to a 48px icon-only column when toggled, instead of disappearing completely. Icons stay visible with tooltips, labels and section headers are hidden. Smooth width transition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: improve settings button alignment and styling in sidebar Match goose2.0's footer pattern with proper padding, full-opacity border, flex alignment, and smooth transition between states. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add home screen with clock, greeting, and hidden status bar Home screen shows a live digital clock, time-based greeting, and placeholder chat input. Status bar hides when on the home view (no active tab), matching goose2.0's clean home experience. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: improve home screen to match goose2.0, add chat view Home screen: large mono clock with AM/PM, time-based greeting, goose2.0-style chat input (rounded-2xl, shadow-lg, model badge). Chat view: input at bottom with scrollable messages area. Home vs chat differentiated by active tab state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: chat view now fills the full available height Remove flex from main element so child components can properly resolve h-full against the block parent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add testing infrastructure (Vitest, Playwright, CI) Testing: - Vitest for unit/component tests with jsdom + React Testing Library - 13 tests across 3 test files (ThemeProvider, cn utility, HomeScreen) - Playwright for E2E smoke tests (4 tests) - Test setup with matchMedia mock for jsdom Tooling: - Just targets: test, test-watch, test-coverage, test-e2e - Lefthook pre-push now runs unit tests - CI gate includes tests CI/CD: - GitHub Actions workflow with 4 jobs: lint, test, desktop, rust-lint - Playwright artifacts uploaded on failure - Rust caching for faster builds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve all Biome formatting and lint issues Auto-format all files with Biome. Fix lint issues: - Add type="button" to all interactive buttons - Replace span[role=button] with proper button element - Add dialog role to settings modal - Biome-ignore for standard accessibility !important pattern All checks pass: pnpm check, pnpm test, pnpm build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add src-tauri/target/ to .gitignore Prevent Rust/Tauri build artifacts from being committed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: exclude Tauri generated schemas from Biome formatting * chore: update .gitignore for full project coverage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pin CI actions to commit SHAs for Semgrep compliance Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve CI failures and improve lefthook auto-formatting - Fix dtolnay/rust-toolchain action SHA (was invalid, causing all Rust CI jobs to fail) - Fix Biome folder ignore pattern (remove trailing /**) - Update lefthook pre-commit to auto-format with biome instead of just checking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use vite preview directly with explicit host for E2E server The Playwright webServer was using `pnpm preview --port 4173` which invoked vite preview without --host, causing it to bind to localhost (IPv6 ::1) instead of 127.0.0.1. This caused ERR_CONNECTION_REFUSED in CI and locally. Switch to npx vite preview with --host 127.0.0.1 and --strictPort to ensure reliable binding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: gracefully handle non-Tauri browser environment Check for Tauri runtime before calling window APIs so the app renders correctly in browser-based E2E tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: make E2E smoke tests more robust with longer timeouts - Add 10s timeout for element visibility (CI can be slow to render) - Use .first() for model badge to handle multiple matches - Fix strict mode violation for duplicate text matches Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: smooth transition for status bar show/hide Status bar now slides and fades in/out with a 300ms transition instead of appearing/disappearing abruptly when switching views. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use dynamic import for Tauri API to prevent browser crash Static import of @tauri-apps/api/window fails at module load time in non-Tauri environments. Use dynamic import() guarded by __TAURI_INTERNALS__ check so the app renders in Playwright. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use python http.server for E2E tests, start with empty tabs Switch Playwright webServer from npx vite preview to python3 http.server for more reliable static file serving in CI. Start app with no tabs open (home screen by default). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: align E2E setup with Sprout's proven configuration - Use url instead of port for webServer readiness check - Remove -b 127.0.0.1 flag from python http.server command - Add explicit cwd to webServer config - Use pnpm exec instead of npx for Playwright in CI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove unused @tauri-apps/plugin-opener JS dependency The Rust-side tauri-plugin-opener is still registered, but the JS binding package was never imported from frontend code. Removing it keeps the dependency list honest. Dark mode colors already match goose2.0 — no changes needed there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,142 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm check
|
||||
- run: pnpm typecheck
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm test
|
||||
|
||||
desktop:
|
||||
name: Desktop Build & E2E
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
|
||||
- name: Cache Rust
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm build
|
||||
|
||||
- name: Check Tauri
|
||||
run: cd src-tauri && cargo check
|
||||
|
||||
- name: Clippy
|
||||
run: cd src-tauri && cargo clippy -- -D warnings
|
||||
|
||||
- name: Format check
|
||||
run: cd src-tauri && cargo fmt --check
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E smoke tests
|
||||
run: pnpm exec playwright test --project=smoke
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 7
|
||||
|
||||
rust-lint:
|
||||
name: Rust Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache Rust
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Format check
|
||||
run: cd src-tauri && cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
run: cd src-tauri && cargo clippy -- -D warnings
|
||||
@@ -0,0 +1,42 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Rust/Tauri build artifacts
|
||||
/target/
|
||||
src-tauri/target/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editor / IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.*.sw?
|
||||
|
||||
# OS artifacts
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Scratch / working files
|
||||
.scratch/
|
||||
|
||||
# Playwright artifacts
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Testing coverage
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Hermit (toolchain manager cache)
|
||||
.hermit/
|
||||
@@ -0,0 +1,112 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidelines for AI agents (and developers) working on this codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Goose2 is a Tauri 2 + React 19 desktop app. It uses TypeScript strict mode, Vite, and Tailwind CSS 3. The codebase follows a feature-sliced architecture organized under `src/app/`, `src/features/`, and `src/shared/`.
|
||||
|
||||
## Architecture & File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
app/ — App shell, entry point, top-level providers
|
||||
features/ — Feature modules (tabs, sidebar, settings, status)
|
||||
<feature>/
|
||||
ui/ — React components
|
||||
types.ts — Shared types for the feature
|
||||
shared/
|
||||
ui/ — Reusable UI components (button, etc.)
|
||||
lib/ — Utilities (cn.ts for class merging)
|
||||
theme/ — Theme provider, appearance settings
|
||||
styles/ — Global CSS, design tokens
|
||||
hooks/ — Shared hooks
|
||||
api/ — API integration
|
||||
constants/ — Shared constants
|
||||
context/ — Shared contexts
|
||||
```
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
- Use `cn()` from `@/shared/lib/cn` for Tailwind class merging.
|
||||
- Import paths use the `@/` alias (maps to `./src`).
|
||||
- Components are controlled where possible (state lifted to parent).
|
||||
- Use `lucide-react` for icons.
|
||||
- All `<button>` elements must have `type="button"` to prevent form submission.
|
||||
- Use semantic HTML (`<aside>`, `<nav>`, `<header>`, `<main>`).
|
||||
|
||||
## Theming System
|
||||
|
||||
ThemeProvider manages three axes:
|
||||
|
||||
| Axis | Values | Persistence | Mechanism |
|
||||
|--------------|---------------------------------|-----------------|----------------------------------------------|
|
||||
| Theme mode | `light`, `dark`, `system` | localStorage | `.dark` class on `<html>` |
|
||||
| Accent color | Any hex value | localStorage | `--color-accent` CSS variable |
|
||||
| Density | `compact`, `comfortable`, `spacious` | localStorage | `--density-spacing` CSS variable (0.75/1/1.25) |
|
||||
|
||||
- CSS variables are defined in `globals.css` with light/dark variants.
|
||||
- Tailwind config maps CSS variables to semantic color names.
|
||||
- Color palette tokens: `background` (primary/secondary/tertiary), `foreground` (primary/secondary/tertiary), `border`, `ring`, plus semantic variants (`info`, `danger`, `success`, `warning`).
|
||||
|
||||
## Component Patterns
|
||||
|
||||
- Small, focused components — aim for under 200 lines.
|
||||
- Props interfaces live in the component file, or in `types.ts` for shared types.
|
||||
- Use `forwardRef` for components that need ref forwarding (React 19 makes this optional, but the pattern is still used).
|
||||
- Animations: CSS transitions via Tailwind classes; respect `prefers-reduced-motion`.
|
||||
- Entrance animations: use the `isLoaded` state pattern with `useEffect` + short timeout.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- ARIA roles on interactive elements (`role="tab"`, `role="tablist"`, `role="status"`).
|
||||
- `aria-label` on icon-only buttons.
|
||||
- `aria-hidden` on visually hidden content.
|
||||
- `aria-selected` on selectable items.
|
||||
- Color-only indicators must have text alternatives.
|
||||
- `prefers-reduced-motion` is respected globally.
|
||||
|
||||
## Tauri Integration
|
||||
|
||||
- The window starts hidden and is shown via `getCurrentWindow().show()` after React mounts.
|
||||
- Use `data-tauri-drag-region` on header areas for window dragging.
|
||||
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
|
||||
- `tauri-plugin-window-state` persists window size and position.
|
||||
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
|
||||
|
||||
## Tooling
|
||||
|
||||
| Tool | Purpose |
|
||||
|-------------|-------------------------------------------------|
|
||||
| **Hermit** | Manages toolchain (node, pnpm, biome, just, lefthook) |
|
||||
| **Just** | Task runner (`just dev`, `just build`, `just check`) |
|
||||
| **Lefthook**| Git hooks (pre-commit, pre-push) |
|
||||
| **Biome** | Linting and formatting |
|
||||
| **pnpm** | Package manager |
|
||||
|
||||
## Testing (Planned)
|
||||
|
||||
- E2E tests with Playwright or similar.
|
||||
- Component tests with Vitest + React Testing Library.
|
||||
- File size enforcement via `scripts/check-file-sizes.mjs`.
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- `react` 19.1, `react-dom` 19.1
|
||||
- `@tauri-apps/api` 2.x
|
||||
- `@tanstack/react-query` 5.x
|
||||
- `tailwindcss` 3.x with `tailwindcss-animate`
|
||||
- `lucide-react` for icons
|
||||
- `class-variance-authority` for component variants
|
||||
- `clsx` + `tailwind-merge` for class merging
|
||||
- `@radix-ui/react-slot` for polymorphic components
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't import from `../` across feature boundaries — use `@/` paths.
|
||||
- Don't put business logic in UI components — extract to hooks or utilities.
|
||||
- Don't use inline styles except for dynamic values (like animation delays).
|
||||
- Don't add dependencies without checking if an existing one covers the need.
|
||||
- Don't skip `type="button"` on buttons.
|
||||
- Don't use color-only indicators without text alternatives.
|
||||
- Never use `--no-verify` when pushing — fix the underlying lint/hook issues.
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1 @@
|
||||
hermit
|
||||
@@ -0,0 +1,7 @@
|
||||
# Hermit environment
|
||||
|
||||
This is a [Hermit](https://github.com/cashapp/hermit) bin directory.
|
||||
|
||||
The symlinks in this directory are managed by Hermit and will automatically
|
||||
download and install Hermit itself as well as packages. These packages are
|
||||
local to this environment.
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# This file must be used with "source bin/activate-hermit" from bash or zsh.
|
||||
# You cannot run it directly
|
||||
#
|
||||
# THIS FILE IS GENERATED; DO NOT MODIFY
|
||||
|
||||
if [ "${BASH_SOURCE-}" = "$0" ]; then
|
||||
echo "You must source this script: \$ source $0" >&2
|
||||
exit 33
|
||||
fi
|
||||
|
||||
BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
|
||||
if "${BIN_DIR}/hermit" noop > /dev/null; then
|
||||
eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"
|
||||
|
||||
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
|
||||
hash -r 2>/dev/null
|
||||
fi
|
||||
|
||||
echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
|
||||
fi
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
# This file must be sourced with "source bin/activate-hermit.fish" from Fish shell.
|
||||
# You cannot run it directly.
|
||||
#
|
||||
# THIS FILE IS GENERATED; DO NOT MODIFY
|
||||
|
||||
if status is-interactive
|
||||
set BIN_DIR (dirname (status --current-filename))
|
||||
|
||||
if "$BIN_DIR/hermit" noop > /dev/null
|
||||
# Source the activation script generated by Hermit
|
||||
"$BIN_DIR/hermit" activate "$BIN_DIR/.." | source
|
||||
|
||||
# Clear the command cache if applicable
|
||||
functions -c > /dev/null 2>&1
|
||||
|
||||
# Display activation message
|
||||
echo "Hermit environment $($HERMIT_ENV/bin/hermit env HERMIT_ENV) activated"
|
||||
end
|
||||
else
|
||||
echo "You must source this script: source $argv[0]" >&2
|
||||
exit 33
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
.biome-2.4.9.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# THIS FILE IS GENERATED; DO NOT MODIFY
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
export HERMIT_USER_HOME=~
|
||||
|
||||
if [ -z "${HERMIT_STATE_DIR}" ]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit"
|
||||
;;
|
||||
Linux)
|
||||
export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
|
||||
HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
|
||||
export HERMIT_CHANNEL
|
||||
export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}
|
||||
|
||||
if [ ! -x "${HERMIT_EXE}" ]; then
|
||||
echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
|
||||
INSTALL_SCRIPT="$(mktemp)"
|
||||
# This value must match that of the install script
|
||||
INSTALL_SCRIPT_SHA256="09ed936378857886fd4a7a4878c0f0c7e3d839883f39ca8b4f2f242e3126e1c6"
|
||||
if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then
|
||||
curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}"
|
||||
else
|
||||
# Install script is versioned by its sha256sum value
|
||||
curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}"
|
||||
# Verify install script's sha256sum
|
||||
openssl dgst -sha256 "${INSTALL_SCRIPT}" | \
|
||||
awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \
|
||||
'$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}'
|
||||
fi
|
||||
/bin/bash "${INSTALL_SCRIPT}" 1>&2
|
||||
fi
|
||||
|
||||
exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
|
||||
@@ -0,0 +1,2 @@
|
||||
github-token-auth {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.just-1.48.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.lefthook-2.1.4.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.node-24.14.1.pkg
|
||||
@@ -0,0 +1 @@
|
||||
.pnpm-10.33.0.pkg
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**", "!src-tauri/gen"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": false
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/shared/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/shared/ui",
|
||||
"utils": "@/shared/lib/cn",
|
||||
"ui": "@/shared/ui",
|
||||
"lib": "@/shared/lib"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Goose</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
# Default recipe
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# ── Dev Environment ──────────────────────────────────────────
|
||||
|
||||
# Install dependencies
|
||||
setup:
|
||||
pnpm install
|
||||
cd src-tauri && cargo build
|
||||
|
||||
# ── Build & Check ────────────────────────────────────────────
|
||||
|
||||
# Run all checks (lint, format, typecheck, file sizes)
|
||||
check:
|
||||
pnpm check
|
||||
pnpm typecheck
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
pnpm format
|
||||
cd src-tauri && cargo fmt
|
||||
|
||||
# Check formatting without modifying
|
||||
fmt-check:
|
||||
biome format .
|
||||
cd src-tauri && cargo fmt --check
|
||||
|
||||
# Run clippy on Tauri backend
|
||||
clippy:
|
||||
cd src-tauri && cargo clippy -- -D warnings
|
||||
|
||||
# Build the frontend
|
||||
build:
|
||||
pnpm build
|
||||
|
||||
# Check Tauri Rust formatting
|
||||
tauri-fmt-check:
|
||||
cd src-tauri && cargo fmt --check
|
||||
|
||||
# Check Tauri Rust types
|
||||
tauri-check:
|
||||
cd src-tauri && cargo check
|
||||
|
||||
# Full CI gate
|
||||
ci: check clippy test build tauri-check
|
||||
|
||||
# ── Test ─────────────────────────────────────────────────────
|
||||
|
||||
# Run unit/component tests
|
||||
test:
|
||||
pnpm test
|
||||
|
||||
# Run tests in watch mode
|
||||
test-watch:
|
||||
pnpm test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
pnpm test:coverage
|
||||
|
||||
# Run E2E smoke tests (builds first)
|
||||
test-e2e:
|
||||
pnpm test:e2e:smoke
|
||||
|
||||
# Run all E2E tests (builds first)
|
||||
test-e2e-all:
|
||||
pnpm test:e2e
|
||||
|
||||
# ── Run ──────────────────────────────────────────────────────
|
||||
|
||||
# Start the desktop app in dev mode
|
||||
dev:
|
||||
pnpm tauri dev
|
||||
|
||||
# Start the desktop app with dev config
|
||||
dev-debug:
|
||||
pnpm tauri dev --config src-tauri/tauri.dev.conf.json
|
||||
|
||||
# Start only the frontend dev server
|
||||
dev-frontend:
|
||||
pnpm dev
|
||||
|
||||
# ── Utilities ────────────────────────────────────────────────
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
cd src-tauri && cargo clean
|
||||
rm -rf dist
|
||||
@@ -0,0 +1,22 @@
|
||||
pre-commit:
|
||||
commands:
|
||||
format:
|
||||
run: biome format --write . && biome check --fix . && git add -u
|
||||
check:
|
||||
run: just check
|
||||
|
||||
pre-push:
|
||||
parallel: true
|
||||
commands:
|
||||
fmt-check:
|
||||
run: just fmt-check
|
||||
clippy:
|
||||
run: just clippy
|
||||
check:
|
||||
run: just check
|
||||
test:
|
||||
run: just test
|
||||
build:
|
||||
run: just build
|
||||
tauri-check:
|
||||
run: just tauri-check
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "goose2",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:file-sizes": "node ./scripts/check-file-sizes.mjs",
|
||||
"lint": "biome lint .",
|
||||
"check": "biome check . && pnpm check:file-sizes",
|
||||
"format": "biome format --write .",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "pnpm build && playwright test",
|
||||
"test:e2e:smoke": "pnpm build && playwright test --project=smoke"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tauri-apps/api": "^2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
"vitest": "^3.2.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"@playwright/test": "^1.52.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
timeout: 30_000,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
["list"],
|
||||
["html", { open: "never", outputFolder: "playwright-report" }],
|
||||
],
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:4173",
|
||||
screenshot: "only-on-failure",
|
||||
trace: "on-first-retry",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "smoke",
|
||||
testMatch: ["**/smoke.spec.ts"],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: "python3 -m http.server 4173 -d dist",
|
||||
cwd: ".",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
url: "http://127.0.0.1:4173",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const DEFAULT_LIMIT = 500;
|
||||
|
||||
// Add narrowly scoped exceptions here with justification
|
||||
const EXCEPTIONS = {};
|
||||
|
||||
const DIRS_TO_CHECK = [
|
||||
{ dir: "src/app", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src/features", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src/shared", glob: /\.[jt]sx?$/ },
|
||||
{ dir: "src-tauri/src", glob: /\.rs$/ },
|
||||
];
|
||||
|
||||
function countLines(filePath) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
return content.split("\n").length;
|
||||
}
|
||||
|
||||
function walkDir(dir, pattern) {
|
||||
const results = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkDir(fullPath, pattern));
|
||||
} else if (pattern.test(entry.name)) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
|
||||
for (const { dir, glob } of DIRS_TO_CHECK) {
|
||||
const files = walkDir(dir, glob);
|
||||
for (const file of files) {
|
||||
const rel = relative(".", file);
|
||||
const limit = EXCEPTIONS[rel]?.limit ?? DEFAULT_LIMIT;
|
||||
const lines = countLines(file);
|
||||
if (lines > limit) {
|
||||
violations.push({ file: rel, lines, limit });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Desktop file size check failed:");
|
||||
for (const v of violations) {
|
||||
console.error(` - ${v.file}: ${v.lines} lines (limit ${v.limit})`);
|
||||
}
|
||||
console.error(
|
||||
"\nSplit the file or add a narrowly scoped exception in `scripts/check-file-sizes.mjs`.",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("File size check passed.");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "goose2"
|
||||
version = "0.1.0"
|
||||
description = "Goose desktop app"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "goose2_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-close",
|
||||
"opener:default",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","opener:default","window-state:allow-restore-state","window-state:allow-save-window-state"]}}
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 159 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 173 B |
|
After Width: | Height: | Size: 373 B |
|
After Width: | Height: | Size: 278 B |
|
After Width: | Height: | Size: 173 B |
|
After Width: | Height: | Size: 238 B |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 514 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 901 B |
|
After Width: | Height: | Size: 714 B |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 902 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 91 B |
|
After Width: | Height: | Size: 115 B |
|
After Width: | Height: | Size: 115 B |
|
After Width: | Height: | Size: 145 B |
|
After Width: | Height: | Size: 103 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 194 B |
|
After Width: | Height: | Size: 115 B |
|
After Width: | Height: | Size: 183 B |
|
After Width: | Height: | Size: 183 B |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 386 B |
@@ -0,0 +1,14 @@
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::default()
|
||||
.with_state_flags(StateFlags::all() & !StateFlags::VISIBLE)
|
||||
.build(),
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
goose2_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Goose",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.goose.app",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
"script": "exec ./node_modules/.bin/vite",
|
||||
"cwd": "..",
|
||||
"wait": false
|
||||
},
|
||||
"devUrl": "http://localhost:1520",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Goose",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"visible": false,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"dragDropEnabled": false,
|
||||
"trafficLightPosition": { "x": 12, "y": 22 }
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"identifier": "com.goose.app.dev",
|
||||
"productName": "Goose Dev"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { AppShell } from "@/app/AppShell";
|
||||
|
||||
export function App() {
|
||||
useEffect(() => {
|
||||
// Dynamic import to avoid crash in non-Tauri environments (e.g., Playwright E2E)
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => {
|
||||
getCurrentWindow()
|
||||
.show()
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <AppShell />;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { TabBar } from "@/features/tabs/ui/TabBar";
|
||||
import { Sidebar } from "@/features/sidebar/ui/Sidebar";
|
||||
import { StatusBar } from "@/features/status/ui/StatusBar";
|
||||
import { HomeScreen } from "@/features/home/ui/HomeScreen";
|
||||
import { ChatView } from "@/features/chat/ui/ChatView";
|
||||
import { SettingsModal } from "@/features/settings/ui/SettingsModal";
|
||||
import type { Tab } from "@/features/tabs/types";
|
||||
|
||||
export function AppShell({ children }: { children?: React.ReactNode }) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [tabs, setTabs] = useState<Tab[]>([]);
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
const isHome = activeTabId === null;
|
||||
|
||||
const handleNewTab = () => {
|
||||
const id = String(Date.now());
|
||||
setTabs((prev) => [...prev, { id, title: "New Chat" }]);
|
||||
setActiveTabId(id);
|
||||
};
|
||||
|
||||
const handleTabClose = (id: string) => {
|
||||
setTabs((prev) => {
|
||||
const next = prev.filter((t) => t.id !== id);
|
||||
if (activeTabId === id) {
|
||||
setActiveTabId(next[0]?.id ?? null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "," && e.metaKey) {
|
||||
e.preventDefault();
|
||||
setSettingsOpen((prev) => !prev);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col overflow-hidden bg-background text-foreground">
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
onTabSelect={setActiveTabId}
|
||||
onTabClose={handleTabClose}
|
||||
onNewTab={handleNewTab}
|
||||
onHomeClick={() => setActiveTabId(null)}
|
||||
onSidebarToggle={() => setSidebarOpen((prev) => !prev)}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<Sidebar
|
||||
isOpen={sidebarOpen}
|
||||
onSettingsClick={() => setSettingsOpen(true)}
|
||||
/>
|
||||
<main className="min-h-0 min-w-0 flex-1">
|
||||
{children ?? (isHome ? <HomeScreen /> : <ChatView />)}
|
||||
</main>
|
||||
</div>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
isHome ? "max-h-0 opacity-0" : "max-h-8 opacity-100"
|
||||
}`}
|
||||
>
|
||||
<StatusBar
|
||||
modelName="Claude Sonnet 4"
|
||||
tokenCount={0}
|
||||
status="connected"
|
||||
/>
|
||||
</div>
|
||||
{settingsOpen && <SettingsModal onClose={() => setSettingsOpen(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_INTERNALS__?: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ArrowUp } from "lucide-react";
|
||||
|
||||
export function ChatView() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Messages area */}
|
||||
<div className="flex flex-1 flex-col items-center justify-center min-h-0 overflow-y-auto px-4 py-6">
|
||||
<div className="max-w-3xl w-full text-center">
|
||||
<p className="text-sm text-foreground-tertiary">
|
||||
Start a conversation with Goose
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat input at bottom */}
|
||||
<div className="px-4 pb-6 pt-2">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="relative rounded-2xl bg-background-secondary border border-border px-4 pt-4 pb-3 shadow-lg">
|
||||
<div className="mb-3 min-h-[36px] w-full px-1 text-sm leading-relaxed text-foreground-tertiary">
|
||||
Type your message here...
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-xs text-foreground-tertiary">
|
||||
<span className="rounded-md border border-border px-2 py-0.5">
|
||||
Claude Sonnet 4
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-foreground/10 text-foreground-secondary transition-all"
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||