mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa6a6aed3 | ||
|
|
ba054b4dfb | ||
|
|
604b41db9b | ||
|
|
8dd94757d7 | ||
|
|
a99edcc0b2 | ||
|
|
1985bd6669 | ||
|
|
674573938a | ||
|
|
7821a8a8af | ||
|
|
226ece2bdd | ||
|
|
cec0b96adb | ||
|
|
9d64c3e01e | ||
|
|
bb300fa2f8 |
9
.mise.toml
Normal file
9
.mise.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[env]
|
||||
ANDROID_HOME = "{{env.HOME}}/.local/share/mise/installs/android-sdk/1.0"
|
||||
_.path = [
|
||||
"{{env.HOME}}/.local/share/mise/installs/android-sdk/1.0/platform-tools",
|
||||
"{{env.HOME}}/.local/share/mise/installs/android-sdk/1.0/emulator",
|
||||
]
|
||||
|
||||
[tools]
|
||||
java = "17"
|
||||
@@ -1,2 +1,4 @@
|
||||
rust 1.85.1
|
||||
nodejs 22.20.0
|
||||
rust 1.85.1
|
||||
nodejs 22.20.0
|
||||
java 21
|
||||
android-sdk latest
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.18 - 2026-03-06
|
||||
### Added
|
||||
- Added a desktop `Mod+W` shortcut to close the current tab.
|
||||
|
||||
### Improved
|
||||
- New and newly selected terminals now take focus automatically so you can type immediately.
|
||||
- Kept newly created workspaces and projects in a more stable order in the sidebar.
|
||||
- Improved project naming for GitHub remotes and expanded project icon discovery to Phoenix `priv/static` assets.
|
||||
- Updated the website desktop download link to use the universal macOS DMG.
|
||||
|
||||
### Fixed
|
||||
- Restored automatic agent metadata generation for Claude runs.
|
||||
|
||||
## 0.1.17 - 2026-03-06
|
||||
### Added
|
||||
- New workspace-first navigation model with workspace tabs, file tabs, and sortable tab groups.
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@@ -141,12 +141,15 @@ From `packages/app`:
|
||||
|
||||
```bash
|
||||
# development (debug)
|
||||
APP_VARIANT=development npx expo prebuild --platform android --clean --non-interactive
|
||||
APP_VARIANT=development npx expo prebuild --platform android --non-interactive
|
||||
APP_VARIANT=development npx expo run:android --variant=debug
|
||||
|
||||
# production (release)
|
||||
APP_VARIANT=production npx expo prebuild --platform android --clean --non-interactive
|
||||
APP_VARIANT=production npx expo prebuild --platform android --non-interactive
|
||||
APP_VARIANT=production npx expo run:android --variant=release
|
||||
|
||||
# clean native project (when needed)
|
||||
npx expo prebuild --platform android --clean --non-interactive
|
||||
```
|
||||
|
||||
From repo root:
|
||||
@@ -154,9 +157,10 @@ From repo root:
|
||||
```bash
|
||||
npm run android:development
|
||||
npm run android:production
|
||||
npm run android:clean
|
||||
```
|
||||
|
||||
`npm run android:prod` and `npm run android:release` are aliases for `npm run android:production`.
|
||||
`npm run android:release` is an alias for `npm run android:production`.
|
||||
|
||||
### Cloud build + submit (EAS Workflows)
|
||||
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -21571,7 +21571,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"dependencies": {
|
||||
"@boudra/expo-two-way-audio": "^0.1.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -21579,7 +21579,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@floating-ui/react-native": "^0.10.7",
|
||||
"@getpaseo/server": "0.1.17",
|
||||
"@getpaseo/server": "0.1.18",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@lezer/common": "^1.5.0",
|
||||
@@ -21685,11 +21685,11 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/relay": "0.1.17",
|
||||
"@getpaseo/server": "0.1.17",
|
||||
"@getpaseo/relay": "0.1.18",
|
||||
"@getpaseo/server": "0.1.18",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -21739,14 +21739,14 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.6"
|
||||
}
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -21762,12 +21762,12 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "2.0.52",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||
"@deepgram/sdk": "^3.4.0",
|
||||
"@getpaseo/relay": "0.1.17",
|
||||
"@getpaseo/relay": "0.1.18",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/css": "^1.3.0",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
@@ -22112,7 +22112,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.20.3",
|
||||
"@cloudflare/workers-types": "^4.20260114.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/server",
|
||||
@@ -26,9 +26,9 @@
|
||||
"start": "npm run start --workspace=@getpaseo/server",
|
||||
"android": "npm run android --workspace=@getpaseo/app",
|
||||
"android:development": "npm run android:development --workspace=@getpaseo/app",
|
||||
"android:prod": "npm run android:prod --workspace=@getpaseo/app",
|
||||
"android:production": "npm run android:production --workspace=@getpaseo/app",
|
||||
"android:release": "npm run android:prod --workspace=@getpaseo/app",
|
||||
"android:release": "npm run android:production --workspace=@getpaseo/app",
|
||||
"android:clean": "npm run android:clean --workspace=@getpaseo/app",
|
||||
"ios": "npm run ios --workspace=@getpaseo/app",
|
||||
"web": "npm run web --workspace=@getpaseo/app",
|
||||
"dev:desktop": "npm run dev --workspace=@getpaseo/desktop",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import {
|
||||
createAgent,
|
||||
createAgentInRepo,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
setWorkingDirectory,
|
||||
} from "./helpers/app";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { getWorkspaceTabTestIds } from "./helpers/workspace-tabs";
|
||||
import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui";
|
||||
|
||||
function visibleTestId(page: Page, testId: string) {
|
||||
@@ -264,6 +266,23 @@ async function readCurrentTerminalBuffer(page: Page): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function expectTerminalFocused(page: Page): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const surface = document.querySelector<HTMLElement>(
|
||||
'[data-testid="terminal-surface"]'
|
||||
);
|
||||
if (!surface) {
|
||||
return false;
|
||||
}
|
||||
const active = document.activeElement;
|
||||
return active instanceof HTMLElement && surface.contains(active);
|
||||
});
|
||||
})
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function expectCurrentTerminalBufferToContain(page: Page, marker: string): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => await readCurrentTerminalBuffer(page), { timeout: 30000 })
|
||||
@@ -376,11 +395,7 @@ test("new terminal does not inherit output from the previously selected terminal
|
||||
directory: repo.path,
|
||||
prompt: "hello",
|
||||
});
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
serverId,
|
||||
targetWorkspacePath: repo.path,
|
||||
});
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, repo.path));
|
||||
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
@@ -403,6 +418,74 @@ test("new terminal does not inherit output from the previously selected terminal
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace terminal tabs auto-focus on create and switch on desktop web", async ({
|
||||
page,
|
||||
}) => {
|
||||
const repo = await createTempGitRepo("paseo-e2e-terminal-focus-");
|
||||
|
||||
try {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set.");
|
||||
}
|
||||
|
||||
await createAgentInRepo(page, {
|
||||
directory: repo.path,
|
||||
prompt: "hello",
|
||||
});
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, repo.path));
|
||||
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await page.getByTestId("workspace-new-terminal-tab").first().click();
|
||||
await waitForTerminalAttachToSettle(page);
|
||||
await expectTerminalFocused(page);
|
||||
|
||||
const firstMarker = `terminal-focus-one-${Date.now()}`;
|
||||
await page.keyboard.type(`echo ${firstMarker}`, { delay: 1 });
|
||||
await page.keyboard.press("Enter");
|
||||
await expectCurrentTerminalBufferToContain(page, firstMarker);
|
||||
|
||||
const terminalTabIdsBeforeSecondCreate = (await getWorkspaceTabTestIds(page)).filter((id) =>
|
||||
id.startsWith("workspace-tab-terminal_")
|
||||
);
|
||||
|
||||
await page.getByTestId("workspace-new-terminal-tab").first().click();
|
||||
await waitForTerminalAttachToSettle(page);
|
||||
await expectTerminalFocused(page);
|
||||
|
||||
const terminalTabIdsAfterSecondCreate = (await getWorkspaceTabTestIds(page)).filter((id) =>
|
||||
id.startsWith("workspace-tab-terminal_")
|
||||
);
|
||||
const secondTerminalTabId = terminalTabIdsAfterSecondCreate.find(
|
||||
(id) => !terminalTabIdsBeforeSecondCreate.includes(id)
|
||||
);
|
||||
const firstTerminalTabId = terminalTabIdsBeforeSecondCreate[0];
|
||||
|
||||
if (!firstTerminalTabId || !secondTerminalTabId) {
|
||||
throw new Error("Expected two distinct terminal tabs to exist.");
|
||||
}
|
||||
|
||||
const secondMarker = `terminal-focus-two-${Date.now()}`;
|
||||
await page.keyboard.type(`echo ${secondMarker}`, { delay: 1 });
|
||||
await page.keyboard.press("Enter");
|
||||
await expectCurrentTerminalBufferToContain(page, secondMarker);
|
||||
|
||||
await page.getByTestId(firstTerminalTabId).first().click();
|
||||
await waitForTerminalAttachToSettle(page);
|
||||
await expectTerminalFocused(page);
|
||||
await expectCurrentTerminalBufferToContain(page, firstMarker);
|
||||
|
||||
await page.getByTestId(secondTerminalTabId).first().click();
|
||||
await waitForTerminalAttachToSettle(page);
|
||||
await expectTerminalFocused(page);
|
||||
await expectCurrentTerminalBufferToContain(page, secondMarker);
|
||||
} finally {
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("terminal reattaches cleanly after heavy output and tab switches", async ({ page }) => {
|
||||
const repo = await createTempGitRepo("paseo-e2e-terminal-reattach-");
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"main": "index.ts",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "npm run android:development",
|
||||
"android:development": "APP_VARIANT=development expo prebuild --platform android --clean --non-interactive && APP_VARIANT=development expo run:android --variant=debug",
|
||||
"android:production": "APP_VARIANT=production expo prebuild --platform android --clean --non-interactive && APP_VARIANT=production expo run:android --variant=release",
|
||||
"android:prod": "npm run android:production",
|
||||
"android:development": "APP_VARIANT=development expo prebuild --platform android --non-interactive && APP_VARIANT=development expo run:android --variant=debug",
|
||||
"android:production": "APP_VARIANT=production expo prebuild --platform android --non-interactive && APP_VARIANT=production expo run:android --variant=release",
|
||||
"android:release": "npm run android:production",
|
||||
"android:clean": "expo prebuild --platform android --clean --non-interactive",
|
||||
"ios": "expo run:ios",
|
||||
"ios:release": "expo run:ios --configuration Release",
|
||||
"web": "expo start --web",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@floating-ui/react-native": "^0.10.7",
|
||||
"@getpaseo/server": "0.1.17",
|
||||
"@getpaseo/server": "0.1.18",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@lezer/common": "^1.5.0",
|
||||
|
||||
@@ -55,6 +55,7 @@ import { buildSidebarShortcutModel } from '@/utils/sidebar-shortcuts'
|
||||
import { hasVisibleOrderChanged, mergeWithRemainder } from '@/utils/sidebar-reorder'
|
||||
import { decideLongPressMove } from '@/utils/sidebar-gesture-arbitration'
|
||||
import { confirmDialog } from '@/utils/confirm-dialog'
|
||||
import { projectIconPlaceholderLabelFromDisplayName } from '@/utils/project-display-name'
|
||||
|
||||
const PASEO_WORKTREE_PATH_MARKER = '/.paseo/worktrees'
|
||||
|
||||
@@ -416,6 +417,8 @@ function ProjectHeaderRow({
|
||||
menuController: null,
|
||||
debugId: `project:${project.projectKey}`,
|
||||
})
|
||||
const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(displayName)
|
||||
const placeholderInitial = placeholderLabel.charAt(0).toUpperCase()
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (interaction.didLongPressRef.current) {
|
||||
@@ -449,7 +452,7 @@ function ProjectHeaderRow({
|
||||
<Image source={{ uri: iconDataUri }} style={styles.projectIcon} />
|
||||
) : (
|
||||
<View style={styles.projectIconFallback}>
|
||||
<Text style={styles.projectIconFallbackText}>{displayName.charAt(0).toUpperCase()}</Text>
|
||||
<Text style={styles.projectIconFallbackText}>{placeholderInitial}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import "@xterm/xterm/css/xterm.css";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import type { PendingTerminalModifiers } from "../utils/terminal-keys";
|
||||
import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime";
|
||||
import { focusWithRetries } from "../utils/web-focus";
|
||||
import {
|
||||
summarizeTerminalText,
|
||||
terminalDebugLog,
|
||||
@@ -343,7 +344,19 @@ export default function TerminalEmulator({
|
||||
if (focusRequestToken <= 0) {
|
||||
return;
|
||||
}
|
||||
runtimeRef.current?.focus();
|
||||
return focusWithRetries({
|
||||
focus: () => {
|
||||
runtimeRef.current?.focus();
|
||||
},
|
||||
isFocused: () => {
|
||||
const root = rootRef.current;
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
const active = typeof document !== "undefined" ? document.activeElement : null;
|
||||
return active instanceof HTMLElement && root.contains(active);
|
||||
},
|
||||
});
|
||||
}, [focusRequestToken]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -214,6 +214,7 @@ export function TerminalPane({
|
||||
const selectedTerminalIdRef = useRef<string | null>(selectedTerminalId);
|
||||
const pendingTerminalInputRef = useRef<PendingTerminalInput[]>([]);
|
||||
const keyboardRefitTimeoutsRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
||||
const lastAutoFocusKeyRef = useRef<string | null>(null);
|
||||
|
||||
const updateSelectedTerminalId = useCallback(
|
||||
(next: string | null) => {
|
||||
@@ -279,6 +280,21 @@ export function TerminalPane({
|
||||
setResizeRequestToken((current) => current + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile || !isScreenFocused || !selectedTerminalId) {
|
||||
lastAutoFocusKeyRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextFocusKey = `${scopeKey}:${selectedTerminalId}`;
|
||||
if (lastAutoFocusKeyRef.current === nextFocusKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastAutoFocusKeyRef.current = nextFocusKey;
|
||||
requestTerminalFocus();
|
||||
}, [isMobile, isScreenFocused, requestTerminalFocus, scopeKey, selectedTerminalId]);
|
||||
|
||||
const clearKeyboardRefitTimeouts = useCallback(() => {
|
||||
if (keyboardRefitTimeoutsRef.current.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
appendMissingOrderKeys,
|
||||
applyStoredOrdering,
|
||||
buildSidebarProjectsFromWorkspaces,
|
||||
} from './use-sidebar-workspaces-list'
|
||||
@@ -46,6 +47,28 @@ describe('applyStoredOrdering', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('appendMissingOrderKeys', () => {
|
||||
it('appends unseen keys while preserving existing order', () => {
|
||||
const result = appendMissingOrderKeys({
|
||||
currentOrder: ['project-b', 'project-a'],
|
||||
visibleKeys: ['project-a', 'project-b', 'project-c'],
|
||||
})
|
||||
|
||||
expect(result).toEqual(['project-b', 'project-a', 'project-c'])
|
||||
})
|
||||
|
||||
it('returns the same array when there are no unseen keys', () => {
|
||||
const currentOrder = ['project-a', 'project-b']
|
||||
|
||||
const result = appendMissingOrderKeys({
|
||||
currentOrder,
|
||||
visibleKeys: ['project-b', 'project-a'],
|
||||
})
|
||||
|
||||
expect(result).toBe(currentOrder)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSidebarProjectsFromWorkspaces', () => {
|
||||
it('uses workspace descriptor name and status directly', () => {
|
||||
const workspaces: WorkspaceDescriptor[] = [
|
||||
@@ -70,4 +93,148 @@ describe('buildSidebarProjectsFromWorkspaces', () => {
|
||||
expect(projects[0]?.workspaces[0]?.name).toBe('feat/hard-cut')
|
||||
expect(projects[0]?.workspaces[0]?.statusBucket).toBe('failed')
|
||||
})
|
||||
|
||||
it('preserves stored project order even when activity changes', () => {
|
||||
const initialWorkspaces: WorkspaceDescriptor[] = [
|
||||
{
|
||||
id: '/repo/b',
|
||||
projectId: 'project-b',
|
||||
name: 'feat/b',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: '/repo/a',
|
||||
projectId: 'project-a',
|
||||
name: 'feat/a',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
},
|
||||
]
|
||||
|
||||
const seededOrder = appendMissingOrderKeys({
|
||||
currentOrder: [],
|
||||
visibleKeys: buildSidebarProjectsFromWorkspaces({
|
||||
serverId: 'srv',
|
||||
workspaces: initialWorkspaces,
|
||||
projectOrder: [],
|
||||
workspaceOrderByScope: {},
|
||||
}).map((project) => project.projectKey),
|
||||
})
|
||||
|
||||
const updatedProjects = buildSidebarProjectsFromWorkspaces({
|
||||
serverId: 'srv',
|
||||
workspaces: [
|
||||
{
|
||||
id: '/repo/b',
|
||||
projectId: 'project-b',
|
||||
name: 'feat/b',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: '/repo/a',
|
||||
projectId: 'project-a',
|
||||
name: 'feat/a',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-03T00:00:00.000Z'),
|
||||
},
|
||||
],
|
||||
projectOrder: seededOrder,
|
||||
workspaceOrderByScope: {},
|
||||
})
|
||||
|
||||
expect(updatedProjects.map((project) => project.projectKey)).toEqual(['project-b', 'project-a'])
|
||||
})
|
||||
|
||||
it('appends new projects after the stored project order', () => {
|
||||
const projects = buildSidebarProjectsFromWorkspaces({
|
||||
serverId: 'srv',
|
||||
workspaces: [
|
||||
{
|
||||
id: '/repo/c',
|
||||
projectId: 'project-c',
|
||||
name: 'feat/c',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-04T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: '/repo/b',
|
||||
projectId: 'project-b',
|
||||
name: 'feat/b',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: '/repo/a',
|
||||
projectId: 'project-a',
|
||||
name: 'feat/a',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
},
|
||||
],
|
||||
projectOrder: ['project-b', 'project-a', 'project-c'],
|
||||
workspaceOrderByScope: {},
|
||||
})
|
||||
|
||||
expect(projects.map((project) => project.projectKey)).toEqual(['project-b', 'project-a', 'project-c'])
|
||||
})
|
||||
|
||||
it('preserves stored workspace order when workspace activity changes', () => {
|
||||
const initialProjects = buildSidebarProjectsFromWorkspaces({
|
||||
serverId: 'srv',
|
||||
workspaces: [
|
||||
{
|
||||
id: '/repo/main',
|
||||
projectId: 'project-1',
|
||||
name: 'main',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: '/repo/feature',
|
||||
projectId: 'project-1',
|
||||
name: 'feature',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
},
|
||||
],
|
||||
projectOrder: ['project-1'],
|
||||
workspaceOrderByScope: {},
|
||||
})
|
||||
|
||||
const seededWorkspaceOrder = appendMissingOrderKeys({
|
||||
currentOrder: [],
|
||||
visibleKeys: initialProjects[0]?.workspaces.map((workspace) => workspace.workspaceKey) ?? [],
|
||||
})
|
||||
|
||||
const projects = buildSidebarProjectsFromWorkspaces({
|
||||
serverId: 'srv',
|
||||
workspaces: [
|
||||
{
|
||||
id: '/repo/main',
|
||||
projectId: 'project-1',
|
||||
name: 'main',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: '/repo/feature',
|
||||
projectId: 'project-1',
|
||||
name: 'feature',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-01-03T00:00:00.000Z'),
|
||||
},
|
||||
],
|
||||
projectOrder: ['project-1'],
|
||||
workspaceOrderByScope: {
|
||||
'srv::project-1': seededWorkspaceOrder,
|
||||
},
|
||||
})
|
||||
|
||||
expect(projects[0]?.workspaces.map((workspace) => workspace.workspaceId)).toEqual([
|
||||
'/repo/main',
|
||||
'/repo/feature',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'
|
||||
import { useSessionStore } from '@/stores/session-store'
|
||||
import { getHostRuntimeStore } from '@/runtime/host-runtime'
|
||||
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
|
||||
@@ -217,6 +217,23 @@ export function applyStoredOrdering<T>(input: {
|
||||
return ordered
|
||||
}
|
||||
|
||||
export function appendMissingOrderKeys(input: {
|
||||
currentOrder: string[]
|
||||
visibleKeys: string[]
|
||||
}): string[] {
|
||||
if (input.visibleKeys.length === 0) {
|
||||
return input.currentOrder
|
||||
}
|
||||
|
||||
const existingKeys = new Set(input.currentOrder)
|
||||
const missingKeys = input.visibleKeys.filter((key) => !existingKeys.has(key))
|
||||
if (missingKeys.length === 0) {
|
||||
return input.currentOrder
|
||||
}
|
||||
|
||||
return [...input.currentOrder, ...missingKeys]
|
||||
}
|
||||
|
||||
function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string {
|
||||
return `${serverId.trim()}::${projectKey.trim()}`
|
||||
}
|
||||
@@ -296,6 +313,35 @@ export function useSidebarWorkspacesList(options?: {
|
||||
})
|
||||
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces])
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId || projects.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextProjectOrder = appendMissingOrderKeys({
|
||||
currentOrder: persistedProjectOrder,
|
||||
visibleKeys: projects.map((project) => project.projectKey),
|
||||
})
|
||||
if (nextProjectOrder !== persistedProjectOrder) {
|
||||
useSidebarOrderStore.getState().setProjectOrder(serverId, nextProjectOrder)
|
||||
}
|
||||
|
||||
for (const project of projects) {
|
||||
const workspaceOrderScopeKey = getWorkspaceOrderScopeKey(serverId, project.projectKey)
|
||||
const persistedWorkspaceOrder =
|
||||
persistedWorkspaceOrderByScope[workspaceOrderScopeKey] ?? EMPTY_ORDER
|
||||
const nextWorkspaceOrder = appendMissingOrderKeys({
|
||||
currentOrder: persistedWorkspaceOrder,
|
||||
visibleKeys: project.workspaces.map((workspace) => workspace.workspaceKey),
|
||||
})
|
||||
if (nextWorkspaceOrder !== persistedWorkspaceOrder) {
|
||||
useSidebarOrderStore
|
||||
.getState()
|
||||
.setWorkspaceOrder(serverId, project.projectKey, nextWorkspaceOrder)
|
||||
}
|
||||
}
|
||||
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, projects, serverId])
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
if (!isActive || !serverId || connectionStatus !== 'online' || !enabled) {
|
||||
return
|
||||
|
||||
@@ -200,7 +200,7 @@ describe("keyboard-shortcuts", () => {
|
||||
expect(match?.action).toBe("workspace.tab.new");
|
||||
});
|
||||
|
||||
it("matches Alt+Shift+W to close current tab", () => {
|
||||
it("matches Alt+Shift+W to close current tab on web", () => {
|
||||
const match = resolveKeyboardShortcut({
|
||||
event: keyboardEvent({
|
||||
key: "W",
|
||||
@@ -208,7 +208,20 @@ describe("keyboard-shortcuts", () => {
|
||||
altKey: true,
|
||||
shiftKey: true,
|
||||
}),
|
||||
context: shortcutContext(),
|
||||
context: shortcutContext({ isTauri: false }),
|
||||
});
|
||||
|
||||
expect(match?.action).toBe("workspace.tab.close.current");
|
||||
});
|
||||
|
||||
it("matches Mod+W to close current tab on tauri", () => {
|
||||
const match = resolveKeyboardShortcut({
|
||||
event: keyboardEvent({
|
||||
key: "w",
|
||||
code: "KeyW",
|
||||
metaKey: true,
|
||||
}),
|
||||
context: shortcutContext({ isMac: true, isTauri: true }),
|
||||
});
|
||||
|
||||
expect(match?.action).toBe("workspace.tab.close.current");
|
||||
@@ -348,6 +361,11 @@ describe("keyboard-shortcut help sections", () => {
|
||||
"shift",
|
||||
"1-9",
|
||||
]);
|
||||
expect(findRow(sections, "workspace-tab-close-current")?.keys).toEqual([
|
||||
"alt",
|
||||
"shift",
|
||||
"W",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses tauri defaults for workspace and tab jump", () => {
|
||||
@@ -359,6 +377,10 @@ describe("keyboard-shortcut help sections", () => {
|
||||
expect(findRow(sections, "new-agent")?.keys).toEqual(["mod", "shift", "O"]);
|
||||
expect(findRow(sections, "workspace-jump-index")?.keys).toEqual(["mod", "1-9"]);
|
||||
expect(findRow(sections, "workspace-tab-jump-index")?.keys).toEqual(["alt", "1-9"]);
|
||||
expect(findRow(sections, "workspace-tab-close-current")?.keys).toEqual([
|
||||
"mod",
|
||||
"W",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses mod+period as non-mac left sidebar shortcut", () => {
|
||||
|
||||
@@ -155,7 +155,24 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "workspace-tab-close-current-alt-shift-w",
|
||||
id: "workspace-tab-close-current-mod-w-tauri",
|
||||
action: "workspace.tab.close.current",
|
||||
matches: (event) =>
|
||||
isMod(event) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
(event.code === "KeyW" || event.key.toLowerCase() === "w"),
|
||||
when: (context) => context.isTauri && !context.commandCenterOpen,
|
||||
help: {
|
||||
id: "workspace-tab-close-current",
|
||||
section: "global",
|
||||
label: "Close current tab",
|
||||
keys: ["mod", "W"],
|
||||
when: (context) => context.isTauri,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "workspace-tab-close-current-alt-shift-w-web",
|
||||
action: "workspace.tab.close.current",
|
||||
matches: (event) =>
|
||||
!event.metaKey &&
|
||||
@@ -163,12 +180,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [
|
||||
event.altKey &&
|
||||
event.shiftKey &&
|
||||
(event.code === "KeyW" || event.key.toLowerCase() === "w"),
|
||||
when: (context) => !context.commandCenterOpen,
|
||||
when: (context) => !context.isTauri && !context.commandCenterOpen,
|
||||
help: {
|
||||
id: "workspace-tab-close-current",
|
||||
section: "global",
|
||||
label: "Close current tab",
|
||||
keys: ["alt", "shift", "W"],
|
||||
when: (context) => !context.isTauri,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -53,7 +53,6 @@ import { normalizeAgentSnapshot } from '@/utils/agent-snapshots'
|
||||
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
|
||||
|
||||
const EMPTY_PENDING_PERMISSIONS = new Map()
|
||||
const MAX_INITIAL_AGENT_TITLE_CHARS = 60
|
||||
const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
|
||||
supportsStreaming: true,
|
||||
supportsSessionPersistence: false,
|
||||
@@ -98,22 +97,6 @@ function getValidMode(provider: AgentProvider | undefined, value: string | undef
|
||||
return modes.some((mode) => mode.id === value) ? value : undefined
|
||||
}
|
||||
|
||||
function deriveInitialAgentTitle(prompt: string): string | null {
|
||||
const firstContentLine = prompt
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0)
|
||||
if (!firstContentLine) {
|
||||
return null
|
||||
}
|
||||
const normalized = firstContentLine.replace(/\s+/g, " ").trim()
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
const clamped = normalized.slice(0, MAX_INITIAL_AGENT_TITLE_CHARS).trim()
|
||||
return clamped.length > 0 ? clamped : null
|
||||
}
|
||||
|
||||
type DraftAgentParams = {
|
||||
serverId?: string
|
||||
provider?: string
|
||||
@@ -872,11 +855,9 @@ function DraftAgentScreenContent({
|
||||
const modeId = modeOptions.length > 0 && selectedMode !== '' ? selectedMode : undefined
|
||||
const trimmedModel = selectedModel.trim()
|
||||
const trimmedThinkingOptionId = selectedThinkingOptionId.trim()
|
||||
const derivedTitle = deriveInitialAgentTitle(text)
|
||||
const config: AgentSessionConfig = {
|
||||
provider: selectedProvider,
|
||||
cwd: resolvedWorkingDir,
|
||||
...(derivedTitle ? { title: derivedTitle } : {}),
|
||||
...(modeId ? { modeId } : {}),
|
||||
...(trimmedModel ? { model: trimmedModel } : {}),
|
||||
...(trimmedThinkingOptionId ? { thinkingOptionId: trimmedThinkingOptionId } : {}),
|
||||
|
||||
27
packages/app/src/utils/project-display-name.test.ts
Normal file
27
packages/app/src/utils/project-display-name.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
projectDisplayNameFromProjectId,
|
||||
projectIconPlaceholderLabelFromDisplayName,
|
||||
} from "./project-display-name";
|
||||
|
||||
describe("projectDisplayNameFromProjectId", () => {
|
||||
it("shows owner and repo for GitHub remote ids", () => {
|
||||
expect(projectDisplayNameFromProjectId("remote:github.com/getpaseo/paseo")).toBe(
|
||||
"getpaseo/paseo"
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the trailing directory name for local projects", () => {
|
||||
expect(projectDisplayNameFromProjectId("/Users/me/dev/paseo")).toBe("paseo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("projectIconPlaceholderLabelFromDisplayName", () => {
|
||||
it("uses repo name instead of owner for GitHub-style display names", () => {
|
||||
expect(projectIconPlaceholderLabelFromDisplayName("getpaseo/paseo")).toBe("paseo");
|
||||
});
|
||||
|
||||
it("returns the original display name when it has no path separator", () => {
|
||||
expect(projectIconPlaceholderLabelFromDisplayName("paseo")).toBe("paseo");
|
||||
});
|
||||
});
|
||||
@@ -7,3 +7,15 @@ export function projectDisplayNameFromProjectId(projectId: string): string {
|
||||
const segments = projectId.split(/[\\/]/).filter(Boolean);
|
||||
return segments[segments.length - 1] || projectId;
|
||||
}
|
||||
|
||||
export function projectIconPlaceholderLabelFromDisplayName(
|
||||
displayName: string
|
||||
): string {
|
||||
const trimmedDisplayName = displayName.trim();
|
||||
if (!trimmedDisplayName) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const segments = trimmedDisplayName.split("/").filter(Boolean);
|
||||
return segments[segments.length - 1] || trimmedDisplayName;
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ export function installScrollJankInvestigation(): void {
|
||||
timeoutHandles.set(timeoutId, true);
|
||||
timerStats.timeout.active = timeoutHandles.size;
|
||||
return timeoutId as unknown as ReturnType<typeof setTimeout>;
|
||||
}) as typeof setTimeout;
|
||||
}) as unknown as typeof setTimeout;
|
||||
|
||||
globalThis.clearTimeout = ((timeoutId?: number) => {
|
||||
if (typeof timeoutId === "number" && timeoutHandles.delete(timeoutId)) {
|
||||
@@ -391,7 +391,7 @@ export function installScrollJankInvestigation(): void {
|
||||
intervalHandles.set(intervalId, true);
|
||||
timerStats.interval.active = intervalHandles.size;
|
||||
return intervalId as unknown as ReturnType<typeof setInterval>;
|
||||
}) as typeof setInterval;
|
||||
}) as unknown as typeof setInterval;
|
||||
|
||||
globalThis.clearInterval = ((intervalId?: number) => {
|
||||
if (typeof intervalId === "number" && intervalHandles.delete(intervalId)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"type": "module",
|
||||
"files": [
|
||||
@@ -24,8 +24,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/relay": "0.1.17",
|
||||
"@getpaseo/server": "0.1.17",
|
||||
"@getpaseo/relay": "0.1.18",
|
||||
"@getpaseo/server": "0.1.18",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Tauri wrapper)",
|
||||
"scripts": {
|
||||
|
||||
2
packages/desktop/src-tauri/Cargo.lock
generated
2
packages/desktop/src-tauri/Cargo.lock
generated
@@ -2629,7 +2629,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "paseo"
|
||||
version = "0.1.17"
|
||||
version = "0.1.18"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "paseo"
|
||||
version = "0.1.17"
|
||||
version = "0.1.18"
|
||||
description = "Paseo Desktop"
|
||||
authors = ["moboudra"]
|
||||
license = "MIT"
|
||||
@@ -36,3 +36,4 @@ tauri-plugin-websocket = "2"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>Paseo</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.17</string>
|
||||
<string>0.1.18</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.17</string>
|
||||
<string>0.1.18</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Paseo needs access to your microphone for voice dictation and voice mode.</string>
|
||||
</dict>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Paseo",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"identifier": "dev.paseo.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../../app/dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"description": "Paseo backend server",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
@@ -63,7 +63,7 @@
|
||||
"@ai-sdk/openai": "2.0.52",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||
"@deepgram/sdk": "^3.4.0",
|
||||
"@getpaseo/relay": "0.1.17",
|
||||
"@getpaseo/relay": "0.1.18",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/css": "^1.3.0",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
|
||||
@@ -753,6 +753,93 @@ describe("ClaudeAgentSession redesign invariants", () => {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test("completes a foreground run when only system metadata arrives before the first assistant message", async () => {
|
||||
let step = 0;
|
||||
sdkMocks.query.mockImplementation(() =>
|
||||
createBaseQueryMock(
|
||||
vi.fn(async () => {
|
||||
if (step === 0) {
|
||||
step += 1;
|
||||
return {
|
||||
done: false,
|
||||
value: {
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
session_id: "redesign-metadata-only-session",
|
||||
permissionMode: "default",
|
||||
model: "opus",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step === 1) {
|
||||
step += 1;
|
||||
return {
|
||||
done: false,
|
||||
value: {
|
||||
type: "system",
|
||||
subtype: "hook_response",
|
||||
session_id: "redesign-metadata-only-session",
|
||||
hook_name: "SessionStart:Callback",
|
||||
hook_event: "SessionStart",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step === 2) {
|
||||
step += 1;
|
||||
return {
|
||||
done: false,
|
||||
value: {
|
||||
type: "assistant",
|
||||
message: { content: "assistant output" },
|
||||
},
|
||||
};
|
||||
}
|
||||
if (step === 3) {
|
||||
step += 1;
|
||||
return {
|
||||
done: false,
|
||||
value: {
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
usage: buildUsage(),
|
||||
total_cost_usd: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { done: true, value: undefined };
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const session = await createSession();
|
||||
try {
|
||||
const events = await Promise.race([
|
||||
collectUntilTerminal(session.stream("metadata helper prompt")),
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(
|
||||
() => reject(new Error("Timed out waiting for foreground terminal event")),
|
||||
1_000
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(events.some((event) => event.type === "turn_completed")).toBe(true);
|
||||
|
||||
const assistantText = events
|
||||
.filter(
|
||||
(event): event is Extract<AgentStreamEvent, { type: "timeline" }> =>
|
||||
event.type === "timeline" && event.item.type === "assistant_message"
|
||||
)
|
||||
.map((event) => event.item.text)
|
||||
.join("");
|
||||
expect(assistantText).toContain("assistant output");
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("reuses one autonomous run for unbound stream_event bursts with no foreground run", async () => {
|
||||
const session = await createSession();
|
||||
const internal = session as unknown as {
|
||||
|
||||
@@ -2746,7 +2746,10 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (!foregroundRun || foregroundRun.promptReplaySeen) {
|
||||
return;
|
||||
}
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
// System metadata (init/hook callbacks/etc.) can precede the first prompt
|
||||
// replay for a legitimate foreground run. Treating that as churn strands
|
||||
// one-shot helper runs in autonomous fallback.
|
||||
if (message.type === "system") {
|
||||
return;
|
||||
}
|
||||
this.preReplayMetadataSeen = true;
|
||||
|
||||
@@ -44,6 +44,10 @@ describe("findProjectIcon", () => {
|
||||
expect(PRIORITY_DIRS).toContain("static");
|
||||
expect(PRIORITY_DIRS).toContain("assets");
|
||||
});
|
||||
|
||||
it("includes Phoenix static assets directory", () => {
|
||||
expect(PRIORITY_DIRS).toContain("priv/static");
|
||||
});
|
||||
});
|
||||
|
||||
describe("IGNORED_DIRS", () => {
|
||||
@@ -97,6 +101,14 @@ describe("findProjectIcon", () => {
|
||||
expect(result).toBe(join(tempDir, "static", "favicon.svg"));
|
||||
});
|
||||
|
||||
it("finds icon in Phoenix priv/static directory", async () => {
|
||||
mkdirSync(join(tempDir, "priv", "static"), { recursive: true });
|
||||
writeFileSync(join(tempDir, "priv", "static", "favicon.ico"), "icon content");
|
||||
|
||||
const result = await findProjectIcon(tempDir);
|
||||
expect(result).toBe(join(tempDir, "priv", "static", "favicon.ico"));
|
||||
});
|
||||
|
||||
it("finds icon in assets directory (priority dir)", async () => {
|
||||
mkdirSync(join(tempDir, "assets"));
|
||||
writeFileSync(join(tempDir, "assets", "logo.png"), "icon content");
|
||||
@@ -208,6 +220,14 @@ describe("findProjectIcon", () => {
|
||||
expect(result).toBe(join(tempDir, "packages", "ui", "logo.svg"));
|
||||
});
|
||||
|
||||
it("finds icon in Phoenix app priv/static directory inside monorepo", async () => {
|
||||
mkdirSync(join(tempDir, "apps", "api", "priv", "static"), { recursive: true });
|
||||
writeFileSync(join(tempDir, "apps", "api", "priv", "static", "favicon.ico"), "icon");
|
||||
|
||||
const result = await findProjectIcon(tempDir);
|
||||
expect(result).toBe(join(tempDir, "apps", "api", "priv", "static", "favicon.ico"));
|
||||
});
|
||||
|
||||
it("prioritizes root priority dirs over monorepo dirs", async () => {
|
||||
mkdirSync(join(tempDir, "public"), { recursive: true });
|
||||
mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true });
|
||||
|
||||
@@ -23,9 +23,16 @@ export const ICON_PATTERNS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Directories to search first (in priority order).
|
||||
* Directories or directory paths to search first (in priority order).
|
||||
*/
|
||||
export const PRIORITY_DIRS = ["public", "static", "assets", "images", "img"];
|
||||
export const PRIORITY_DIRS = [
|
||||
"public",
|
||||
"static",
|
||||
"priv/static",
|
||||
"assets",
|
||||
"images",
|
||||
"img",
|
||||
];
|
||||
|
||||
/**
|
||||
* Monorepo package directory patterns to scan (e.g., packages/app, apps/web).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.17",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@ import websitePackage from '../../package.json'
|
||||
import '~/styles.css'
|
||||
|
||||
const desktopVersion = websitePackage.version
|
||||
const macDownloadHref = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}/Paseo_${desktopVersion}_aarch64.dmg`
|
||||
const macDownloadHref = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}/Paseo_${desktopVersion}_universal.dmg`
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
head: () => ({
|
||||
|
||||
Reference in New Issue
Block a user