Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .geminiignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.git/
node_modules/
vendor/
dist/
build/
.next/
*.log
.DS_Store

# Performance Optimization: Ignore large generated files and caches
pnpm-lock.yaml
go.sum
package-lock.json
yarn.lock
.pnpm-store/
.cache/
.turbo/
coverage/
.vercel/
.netlify/
.expo/
5 changes: 4 additions & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ on:
- "v*"
workflow_dispatch:

# Set GITHUB_TOKEN permissions to allow deployment to GitHub Pages
permissions:
contents: write
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs between the currently running and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
Expand All @@ -24,7 +27,7 @@ jobs:
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
fetch-depth: 0 # Not needed if lastUpdated is not enabled

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Thumbs.db
# Temporary files
*.tmp
*.temp
*.bak
.cache/
tmp/

Expand All @@ -109,3 +110,9 @@ data
_main-ref/
.toolkit/
/scripts/vendor
design-env/
GEMINI.md
Design*.md
design*.md
detail-design-2.0-docs/
.gemini/settings.json
5 changes: 5 additions & 0 deletions .husky/check-go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@

export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$HOME/.npm-global/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"

if ! command -v golangci-lint >/dev/null 2>&1; then
echo "golangci-lint not found, skipping Go lint..."
exit 0
fi

echo "Check Go lint..."
golangci-lint run ./...
5 changes: 5 additions & 0 deletions .husky/check-go-test
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@

export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$HOME/.npm-global/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"

if ! command -v go >/dev/null 2>&1; then
echo "go not found, skipping Go test..."
exit 0
fi

echo "Check Go test..."
go test ./...
22 changes: 22 additions & 0 deletions apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,25 @@ typecheck.
real `exports` field. So bundle behaviour is unchanged; only `vue-tsc`
follows the stubs.

The typecheck import graph must still mirror the runtime import graph. When
desktop reuses workspace package exports, source files must import through
the same public module specifiers that Vite bundles at runtime, and the
renderer stubs must model that same surface. Do not paper over `vue-tsc`
errors by swapping imports to aliases or private source paths unless both
Vite and `tsconfig.web.json` intentionally resolve that specifier to the
same module. A resolver mismatch can make one pipeline pass while the other
fails, or make typecheck validate a different module than the one packaged
at runtime.

When you add a new `@memohai/web/*` import in the desktop renderer, add a
matching `declare module` to `web-stubs.d.ts`. The wildcard
`declare module '@memohai/web/*.vue'` already covers any `.vue` SFC reached
through the wildcard `./*` export.

When you import a new component directly from `@memohai/ui`, add that
component to `ui-stubs.d.ts` as well. The package may export it correctly,
but desktop's renderer typecheck only sees the stubbed surface.

## Multi-Window Lifecycle

### Main process (`src/main/index.ts`)
Expand Down Expand Up @@ -552,6 +566,14 @@ list to check.
- **Update both `tsconfig.web.json` paths and `web-stubs.d.ts`** when adding
a new `@memohai/web/foo` import. Forgetting the stub yields
`TS2307: Cannot find module` even though the bundle works.
- **Keep typecheck resolution and runtime resolution isomorphic.** Desktop
renderer code must import reused workspace modules through the same public
specifiers that Vite bundles at runtime, then model that surface in the
stubs. Do not mix in alternate aliases or private source paths unless both
Vite and `tsconfig.web.json` intentionally resolve them to the same target.
- **Update `ui-stubs.d.ts` for direct `@memohai/ui` imports.** A component
exported by the real UI package still needs to exist in the desktop stub
if desktop imports it directly.
- **Run `pnpm --filter @memohai/desktop typecheck` after every renderer
change.** It's fast (only types desktop's own code thanks to the stubs)
and catches the common drift cases (missing stub, wrong store/component
Expand Down
101 changes: 98 additions & 3 deletions apps/desktop/src/renderer/src/settings/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<script setup lang="ts">
import { provide } from 'vue'
import { Toaster, SidebarInset } from '@memohai/ui'
import { provide, computed, toValue } from 'vue'
import { useRoute } from 'vue-router'
import { useQuery } from '@pinia/colada'
import { getBotsById } from '@memohai/sdk'
import {
Toaster, SidebarInset,
Breadcrumb, BreadcrumbList, BreadcrumbItem,
BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator,
} from '@memohai/ui'
import 'vue-sonner/style.css'
import MainLayout from '@memohai/web/layout/main-layout/index.vue'
import SettingsSidebar from '@memohai/web/components/settings-sidebar/index.vue'
Expand All @@ -9,6 +16,51 @@ import { DesktopShellKey } from '@memohai/web/lib/desktop-shell'

provide(DesktopShellKey, true)
useSettingsStore()

const route = useRoute()

// Fetch bot data in the layout to ensure reactive breadcrumb updates for bot-detail
const { data: bot } = useQuery({
key: () => ['bot', route.params.botId as string],
query: async () => {
const { data } = await getBotsById({
path: { id: route.params.botId as string },
throwOnError: true,
})
return data
},
enabled: () => route.name === 'bot-detail' && !!route.params.botId,
})

const breadcrumbs = computed(() => {
const items = []
const matched = route.matched
for (const m of matched) {
if (m.meta && m.meta.breadcrumb) {
let label = ''
// Special case for bot-detail to use the reactive display name
if (m.name === 'bot-detail' && bot.value?.display_name) {
label = bot.value.display_name
} else {
const b = m.meta.breadcrumb
label = typeof b === 'function' ? b(route) : toValue(b)
}

if (label) {
items.push({
label,
to: m.name ? { name: m.name } : m.path,
isLast: false,
})
}
}
}
if (items.length > 0) {
const lastItem = items[items.length - 1]
if (lastItem) lastItem.isLast = true
}
return items
})
</script>

<template>
Expand Down Expand Up @@ -42,7 +94,50 @@ useSettingsStore()
</template>
<template #main>
<SidebarInset class="flex flex-col overflow-hidden">
<section class="flex-1 overflow-y-auto">
<!-- Universal Settings Breadcrumb per Figma 5:937 & 5:807 -->
<header
v-if="breadcrumbs.length > 0"
class="h-10 flex items-center px-6 shrink-0 border-b border-border/40"
>
<Breadcrumb class="w-full">
<BreadcrumbList class="gap-1.5 flex-nowrap">
<template
v-for="(item, index) in breadcrumbs"
:key="index"
>
<BreadcrumbItem
v-if="!item.isLast"
class="shrink-0"
>
<BreadcrumbLink
as-child
class="text-muted-foreground hover:text-foreground transition-colors"
>
<router-link :to="item.to">
<span class="text-[11px] font-medium leading-none">{{ item.label }}</span>
</router-link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator
v-if="!item.isLast"
class="text-muted-foreground/50 shrink-0 select-none"
>
<span class="text-[10px] font-normal">/</span>
</BreadcrumbSeparator>
<BreadcrumbItem
v-else
class="min-w-0 flex-1"
>
<BreadcrumbPage class="text-foreground text-[11px] font-medium truncate leading-none">
{{ item.label }}
</BreadcrumbPage>
</BreadcrumbItem>
</template>
</BreadcrumbList>
</Breadcrumb>
</header>

<section class="flex-1 relative min-h-0">
<router-view v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
Expand Down
23 changes: 16 additions & 7 deletions apps/desktop/src/renderer/src/settings/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createRouter, createMemoryHistory, type RouteRecordRaw } from 'vue-router'
import { SETTINGS_ROUTE_SPECS, SETTINGS_DEFAULT_PATH } from '../shared/settings-routes'
import { h } from 'vue'
import { createRouter, createMemoryHistory, RouterView, type RouteRecordRaw } from 'vue-router'
import { SETTINGS_ROUTE_SPECS, SETTINGS_DEFAULT_PATH, type SettingsRouteSpec } from '../shared/settings-routes'

// Settings-window router. Mirrors the path layout under `/settings/*` from
// @memohai/web's main router so the reused @memohai/web `SettingsSidebar`
Expand All @@ -8,11 +9,19 @@ import { SETTINGS_ROUTE_SPECS, SETTINGS_DEFAULT_PATH } from '../shared/settings-
// into `/settings/bots` (or whatever path the chat window asks for via the
// `settings:navigate` IPC).

const realRoutes: RouteRecordRaw[] = SETTINGS_ROUTE_SPECS.map(({ name, path, loader }) => ({
name,
path,
component: loader,
}))
const mapSpecToRoute = (spec: SettingsRouteSpec): RouteRecordRaw => {
const route = {
path: spec.path,
component: spec.loader ?? { render: () => h(RouterView) },
...(spec.name ? { name: spec.name } : {}),
...(spec.meta ? { meta: spec.meta } : {}),
...(spec.children ? { children: spec.children.map(mapSpecToRoute) } : {}),
} satisfies RouteRecordRaw

return route
}

const realRoutes: RouteRecordRaw[] = SETTINGS_ROUTE_SPECS.map(mapSpecToRoute)

const routes: RouteRecordRaw[] = [
{ path: '/', redirect: SETTINGS_DEFAULT_PATH },
Expand Down
Loading
Loading