diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81ca24b..7143a79 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,12 +102,12 @@ When adding documentation, prefer clarity over comprehensiveness. A short, corre ### Prerequisites -| Dependency | Minimum Version | Notes | -|---------------|-----------------|------------------------------------------| -| Bun | ^1.3.9 | Package manager and runtime | -| Node.js | ^24.13.1 | Also required for some tooling | -| Git | 2.30+ | For version control integration | -| Codex CLI | latest | Required for Codex provider support | +| Dependency | Minimum Version | Notes | +| ---------- | --------------- | ----------------------------------- | +| Bun | ^1.3.9 | Package manager and runtime | +| Node.js | ^24.13.1 | Also required for some tooling | +| Git | 2.30+ | For version control integration | +| Codex CLI | latest | Required for Codex provider support | The project uses **Bun** as its package manager with the `isolated` linker. Do not use `npm` or `yarn`. @@ -172,15 +172,15 @@ If the UI shows no threads after connecting, the issue is likely a WebSocket aut ### Monorepo Packages -| Path | Role | -|-----------------------|-------------------------------------------------------------| +| Path | Role | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `apps/server` | Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, manages provider sessions. | -| `apps/web` | React/Vite UI. Session UX, conversation rendering, client-side state. Connects via WebSocket. | -| `apps/desktop` | Electron desktop shell. Wraps the web app as a native desktop application. | -| `apps/marketing` | Marketing / landing page site. | -| `packages/contracts` | Effect-TS Schema definitions and TypeScript contracts. Schema-only — no runtime logic. | -| `packages/shared` | Runtime utilities consumed by server and web. Uses explicit subpath exports — no barrel index. | -| `packages/effect-acp` | Effect-TS ACP (Agents, Context, Policies) integration. | +| `apps/web` | React/Vite UI. Session UX, conversation rendering, client-side state. Connects via WebSocket. | +| `apps/desktop` | Electron desktop shell. Wraps the web app as a native desktop application. | +| `apps/marketing` | Marketing / landing page site. | +| `packages/contracts` | Effect-TS Schema definitions and TypeScript contracts. Schema-only — no runtime logic. | +| `packages/shared` | Runtime utilities consumed by server and web. Uses explicit subpath exports — no barrel index. | +| `packages/effect-acp` | Effect-TS ACP (Agents, Context, Policies) integration. | ### Event Lifecycle @@ -279,7 +279,7 @@ All code must pass `bun run lint` and `bun run fmt:check` before opening a PR. ### What Makes a Good PR - **Small and focused.** Prefer PRs under 200 lines. Large PRs are difficult to review and more likely to be rejected. -- **Clear motivation.** Explain *what* changed and *why*. If the change is non-obvious, include the reasoning. +- **Clear motivation.** Explain _what_ changed and _why_. If the change is non-obvious, include the reasoning. - **Tests included.** New functionality must include tests. Bug fixes must include a regression test. - **Quality checks pass.** Run `bun run test`, `bun run lint`, `bun run fmt:check`, and `bun run typecheck` before opening the PR. - **UI changes include screenshots.** Before/after images are required. Videos for animation/interaction changes. @@ -379,4 +379,4 @@ This is recorded once per repository and does not need to be repeated for subseq --- -*Thank you for helping make Peak Code better. Every thoughtful contribution — whether a bug report, a documentation fix, or a code change — moves the project forward.* +_Thank you for helping make Peak Code better. Every thoughtful contribution — whether a bug report, a documentation fix, or a code change — moves the project forward._ diff --git a/CONTRIBUTING.zh.md b/CONTRIBUTING.zh.md index 2ba0e9c..0edefb1 100644 --- a/CONTRIBUTING.zh.md +++ b/CONTRIBUTING.zh.md @@ -102,12 +102,12 @@ ### 前置条件 -| 依赖 | 最低版本 | 说明 | -|--------------|------------|------------------------------| -| Bun | ^1.3.9 | 包管理器及运行时 | -| Node.js | ^24.13.1 | 部分工具链所需 | -| Git | 2.30+ | 版本控制集成 | -| Codex CLI | latest | Codex 提供方支持所需 | +| 依赖 | 最低版本 | 说明 | +| --------- | -------- | -------------------- | +| Bun | ^1.3.9 | 包管理器及运行时 | +| Node.js | ^24.13.1 | 部分工具链所需 | +| Git | 2.30+ | 版本控制集成 | +| Codex CLI | latest | Codex 提供方支持所需 | 本项目使用 **Bun** 作为包管理器,采用 `isolated` 链接器。请勿使用 `npm` 或 `yarn`。 @@ -172,15 +172,15 @@ env -u PEAKCODE_AUTH_TOKEN PEAKCODE_PORT_OFFSET=3158 \ ### Monorepo 包结构 -| 路径 | 职责 | -|-----------------------|----------------------------------------------------------------| +| 路径 | 职责 | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `apps/server` | Node.js WebSocket 服务器。包装 Codex app-server(通过 stdio 进行 JSON-RPC 通信),提供 React Web 应用,管理 AI 提供方会话。 | -| `apps/web` | React/Vite UI。会话用户体验、对话渲染、客户端状态管理。通过 WebSocket 连接。 | -| `apps/desktop` | Electron 桌面外壳。将 Web 应用封装为原生桌面应用。 | -| `apps/marketing` | 营销/落地页网站。 | -| `packages/contracts` | Effect-TS Schema 定义和 TypeScript 合约。仅包含 Schema——不含运行时逻辑。 | -| `packages/shared` | 被服务器和 Web 共同使用的运行时工具库。使用显式子路径导出——不使用 barrel index。 | -| `packages/effect-acp` | Effect-TS ACP(Agents, Context, Policies)集成。 | +| `apps/web` | React/Vite UI。会话用户体验、对话渲染、客户端状态管理。通过 WebSocket 连接。 | +| `apps/desktop` | Electron 桌面外壳。将 Web 应用封装为原生桌面应用。 | +| `apps/marketing` | 营销/落地页网站。 | +| `packages/contracts` | Effect-TS Schema 定义和 TypeScript 合约。仅包含 Schema——不含运行时逻辑。 | +| `packages/shared` | 被服务器和 Web 共同使用的运行时工具库。使用显式子路径导出——不使用 barrel index。 | +| `packages/effect-acp` | Effect-TS ACP(Agents, Context, Policies)集成。 | ### 事件生命周期 @@ -379,4 +379,4 @@ I have read the CLA Document and I hereby sign the CLA --- -*感谢您帮助 Peak Code 变得更好。每一份深思熟虑的贡献——无论是 Bug 报告、文档修复还是代码修改——都在推动项目向前发展。* +_感谢您帮助 Peak Code 变得更好。每一份深思熟虑的贡献——无论是 Bug 报告、文档修复还是代码修改——都在推动项目向前发展。_ diff --git a/TODO.md b/TODO.md index 3d85699..f3022f6 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,7 @@ ## Small things - [ ] Submitting new messages should scroll to bottom -- [ ] Only show last 10 threads for a given project +- [x] Only show last 10 threads for a given project - [ ] Thread archiving - [ ] New projects should go on top - [ ] Projects should be sorted by latest thread update diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts index 980eae4..0e20b7d 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.ts @@ -740,9 +740,7 @@ const runCommandHealthProbe = (effect: Effect.Effect - Exit.isSuccess(exit) - ? Result.succeed(exit.value) - : Result.fail(Cause.squash(exit.cause)), + Exit.isSuccess(exit) ? Result.succeed(exit.value) : Result.fail(Cause.squash(exit.cause)), ), ); @@ -1087,9 +1085,7 @@ export const makeCheckClaudeProviderStatus = ( const executable = nonEmptyTrimmed(binaryPath) ?? "claude"; // Probe 1: `claude --version` — is the CLI reachable? - const versionProbe = yield* runCommandHealthProbe( - runClaudeCommand(["--version"], executable), - ); + const versionProbe = yield* runCommandHealthProbe(runClaudeCommand(["--version"], executable)); if (Result.isFailure(versionProbe)) { const error = versionProbe.failure; @@ -1202,9 +1198,7 @@ export const makeCheckGeminiProviderStatus = ( const checkedAt = new Date().toISOString(); const executable = nonEmptyTrimmed(binaryPath) ?? "gemini"; - const versionProbe = yield* runCommandHealthProbe( - runGeminiCommand(["--version"], executable), - ); + const versionProbe = yield* runCommandHealthProbe(runGeminiCommand(["--version"], executable)); if (Result.isFailure(versionProbe)) { const error = versionProbe.failure; @@ -1551,9 +1545,7 @@ export const makeCheckCursorProviderStatus = ( const checkedAt = new Date().toISOString(); const executable = resolveCursorAgentBinaryPath(nonEmptyTrimmed(binaryPath)); - const versionProbe = yield* runCommandHealthProbe( - runCursorCommand(["--version"], executable), - ); + const versionProbe = yield* runCommandHealthProbe(runCursorCommand(["--version"], executable)); if (Result.isFailure(versionProbe)) { const error = versionProbe.failure; diff --git a/apps/web/src/components/BrowserPanel.tsx b/apps/web/src/components/BrowserPanel.tsx index 697b5c5..a2868e9 100644 --- a/apps/web/src/components/BrowserPanel.tsx +++ b/apps/web/src/components/BrowserPanel.tsx @@ -285,9 +285,7 @@ function isBrowserPerfLoggingEnabled(): boolean { } try { - return ( - window.localStorage.getItem("peakcode:browser-perf") === "1" - ); + return window.localStorage.getItem("peakcode:browser-perf") === "1"; } catch { return false; } diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index c3aa28b..d42e0dc 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -31,6 +31,7 @@ import { shouldShowDebugFeatureFlagsMenu, shouldPrunePinnedThreads, shouldClearThreadSelectionOnMouseDown, + SIDEBAR_THREAD_PREVIEW_LIMIT, sortProjectsForSidebar, sortThreadsForSidebar, } from "./Sidebar.logic"; @@ -598,6 +599,10 @@ describe("resolveProjectStatusIndicator", () => { }); describe("getVisibleThreadsForProject", () => { + it("keeps the collapsed project preview aligned with the ten-thread sidebar target", () => { + expect(SIDEBAR_THREAD_PREVIEW_LIMIT).toBe(10); + }); + it("includes the active thread even when it falls below the folded preview", () => { const threads = Array.from({ length: 8 }, (_, index) => makeThread({ diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 9dbe63f..008ac94 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -21,6 +21,7 @@ export { } from "../lib/projectCreateRecovery"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; +export const SIDEBAR_THREAD_PREVIEW_LIMIT = 10; export const SIDEBAR_THREAD_PREWARM_LIMIT = 10; export const DEBUG_FEATURE_FLAGS_MENU_STORAGE_KEY = "peakcode:show-debug-feature-flags-menu"; export type SidebarNewThreadEnvMode = "local" | "worktree"; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index f9de321..40efa5a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -179,7 +179,6 @@ import { useThreadSelectionStore } from "../threadSelectionStore"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; import { isNonEmpty as isNonEmptyString } from "effect/String"; import { - describeAddProjectError, buildProjectThreadTree, deriveSidebarProjectData, extractDuplicateProjectCreateProjectId, @@ -202,6 +201,7 @@ import { shouldShowDebugFeatureFlagsMenu, shouldPrunePinnedThreads, shouldClearThreadSelectionOnMouseDown, + SIDEBAR_THREAD_PREVIEW_LIMIT, sortProjectsForSidebar, sortThreadsForSidebar, } from "./Sidebar.logic"; @@ -251,7 +251,6 @@ import { } from "../lib/projectCreateRecovery"; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; -const THREAD_PREVIEW_LIMIT = 5; function useProjectSortLabels(): Record { const messages = useMessages(); @@ -1231,19 +1230,11 @@ export default function Sidebar() { const removeWorktreeMutation = useMutation(gitRemoveWorktreeMutationOptions({ queryClient })); const { activeProjectId: focusedProjectId } = useFocusedChatContext(); const [addingProject, setAddingProject] = useState(false); - const [newCwd, setNewCwd] = useState(""); const [searchPaletteOpen, setSearchPaletteOpen] = useState(false); const [searchPaletteMode, setSearchPaletteMode] = useState("search"); const [searchPaletteInitialQuery, setSearchPaletteInitialQuery] = useState(null); const [isPickingFolder, setIsPickingFolder] = useState(false); - const [showManualPathInput, setShowManualPathInput] = useState(false); const [isAddingProject, setIsAddingProject] = useState(false); - const [addProjectError, setAddProjectError] = useState(null); - const addProjectErrorMeaning = useMemo( - () => (addProjectError ? describeAddProjectError(addProjectError) : null), - [addProjectError], - ); - const addProjectInputRef = useRef(null); const [renamingThreadId, setRenamingThreadId] = useState(null); const [pendingArchiveConfirmationThreadId, setPendingArchiveConfirmationThreadId] = useState(null); @@ -1843,8 +1834,6 @@ export default function Sidebar() { setIsAddingProject(true); const finishAddingProject = () => { setIsAddingProject(false); - setNewCwd(""); - setAddProjectError(null); setAddingProject(false); }; @@ -1957,16 +1946,6 @@ export default function Sidebar() { ], ); - const handleAddProject = () => { - void addProjectFromPath(newCwd, { createIfMissing: true }).catch((error: unknown) => { - const description = - error instanceof Error ? error.message : "An error occurred while adding the project."; - setAddProjectError(description); - }); - }; - - const canAddProject = newCwd.trim().length > 0 && !isAddingProject; - // Keep the native folder picker and project creation in one awaited flow so // the UI can show whether we're still opening the dialog or creating the project. const handlePickFolder = useCallback(async () => { @@ -1977,11 +1956,9 @@ export default function Sidebar() { const pickedPath = await api.dialogs.pickFolder(); setIsPickingFolder(false); if (pickedPath) { - setAddProjectError(null); await addProjectFromPath(pickedPath).catch((error: unknown) => { const description = error instanceof Error ? error.message : "An error occurred while adding the project."; - setAddProjectError(description); toastManager.add({ type: "error", title: "Unable to add project", @@ -1992,7 +1969,6 @@ export default function Sidebar() { } catch (error) { const description = error instanceof Error ? error.message : "Unable to open the folder picker."; - setAddProjectError(description); toastManager.add({ type: "error", title: "Unable to open folder picker", @@ -2003,8 +1979,6 @@ export default function Sidebar() { }, [isPickingFolder, addProjectFromPath]); const handleStartAddProject = useCallback(() => { - setAddProjectError(null); - setShowManualPathInput(false); setAddingProject((prev) => !prev); }, []); @@ -2023,10 +1997,16 @@ export default function Sidebar() { return; } + if (isElectron) { + void handlePickFolder(); + return; + } + handleStartAddProject(); }, [ appSettings.defaultThreadEnvMode, currentProjectShortcutTargetId, + handlePickFolder, handleNewThread, handleStartAddProject, ]); @@ -3450,7 +3430,7 @@ export default function Sidebar() { entries: visibleChatPreviewEntries, activeEntryId: activeChatPreviewEntry?.rowId, isExpanded: chatThreadListExpanded, - previewLimit: THREAD_PREVIEW_LIMIT, + previewLimit: SIDEBAR_THREAD_PREVIEW_LIMIT, }), [activeChatPreviewEntry?.rowId, chatThreadListExpanded, visibleChatPreviewEntries], ); @@ -3476,7 +3456,7 @@ export default function Sidebar() { expandedThreadListProjectCwds: expandedThreadListsByProject, normalizeProjectCwd: normalizeSidebarProjectThreadListCwd, activeSidebarThreadId: activeSidebarThreadId ?? undefined, - previewLimit: THREAD_PREVIEW_LIMIT, + previewLimit: SIDEBAR_THREAD_PREVIEW_LIMIT, resolveThreadStatus: resolveThreadStatusForSidebar, }), [ @@ -5704,79 +5684,19 @@ export default function Sidebar() { {shouldShowProjectPathEntry && (
- {!showManualPathInput ? ( -
- {isElectron && ( - - )} - -
- ) : ( -
- { - setNewCwd(event.target.value); - setAddProjectError(null); - }} - onKeyDown={(event) => { - if (event.key === "Enter") handleAddProject(); - if (event.key === "Escape") { - setShowManualPathInput(false); - setAddProjectError(null); - } - }} - autoFocus - /> - -
- )} - {addProjectError && ( -
-

{addProjectError}

- {addProjectErrorMeaning && ( -

- {addProjectErrorMeaning} -

- )} -
- )} +
)} @@ -5830,8 +5750,32 @@ export default function Sidebar() { )} {projectEmptyState === "empty" && ( -
- {messages.sidebar.noProjectsYet} +
+
+ {messages.sidebar.noProjectsYet} +
+
+ {messages.sidebar.noProjectsYetDescription} +
+
)} diff --git a/apps/web/src/i18n/messages.ts b/apps/web/src/i18n/messages.ts index 3919fb4..8f429bc 100644 --- a/apps/web/src/i18n/messages.ts +++ b/apps/web/src/i18n/messages.ts @@ -61,6 +61,10 @@ export type Messages = { settings: string; addProject: string; noProjectsYet: string; + noProjectsYetDescription: string; + chooseProjectFolder: string; + openingFolderPicker: string; + addingProject: string; loadingProjects: string; toggleSidebar: string; codeLabel: string; @@ -1079,6 +1083,10 @@ const en: Messages = { settings: "Settings", addProject: "Add project", noProjectsYet: "No projects yet", + noProjectsYetDescription: "Choose a local project folder to start your first thread.", + chooseProjectFolder: "Choose project folder", + openingFolderPicker: "Opening...", + addingProject: "Adding...", loadingProjects: "Loading projects", toggleSidebar: "Toggle thread sidebar", codeLabel: "Code", @@ -2156,6 +2164,10 @@ const zh: Messages = { settings: "设置", addProject: "添加项目", noProjectsYet: "暂无项目", + noProjectsYetDescription: "选择一个本地项目文件夹来开始第一个线程。", + chooseProjectFolder: "选择项目文件夹", + openingFolderPicker: "正在打开...", + addingProject: "正在添加...", loadingProjects: "正在加载项目", toggleSidebar: "切换线程侧边栏", codeLabel: "代码", diff --git a/packages/shared/src/git.ts b/packages/shared/src/git.ts index ec77bad..1214c22 100644 --- a/packages/shared/src/git.ts +++ b/packages/shared/src/git.ts @@ -65,8 +65,7 @@ export function resolveAutoFeatureBranchName( } export function buildPeakcodeBranchName(preferredBranch?: string | null): string { - const normalizedExisting = - preferredBranch?.trim().replace(/^(codex|peakcode)\//i, "") ?? ""; + const normalizedExisting = preferredBranch?.trim().replace(/^(codex|peakcode)\//i, "") ?? ""; return `${WORKTREE_BRANCH_PREFIX}/${sanitizeBranchFragment( normalizedExisting || PEAKCODE_BRANCH_FALLBACK, )}`; diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 2375dac..afdcff9 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -383,7 +383,18 @@ function stageMacIcons(stageResourcesDir: string, verbose: boolean) { })`sips -z 512 512 ${modernIconSource} --out ${iconPngPath}`, ); - yield* generateMacIconSet(legacyIconSource, iconIcnsPath, tmpRoot, path, verbose); + yield* generateMacIconSet(legacyIconSource, iconIcnsPath, tmpRoot, path, verbose).pipe( + Effect.catch((error) => + Effect.gen(function* () { + if (!(yield* fs.exists(iconIcnsPath))) { + return yield* error; + } + yield* Effect.log( + "[desktop-artifact] Reusing staged macOS .icns after iconset generation failed.", + ); + }), + ), + ); if (hasComposerIcon) { // Replace any repo-local placeholder so the staged build always reflects the authored Icon Composer asset.