diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index e839e76b..59b1b058 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -97,6 +97,18 @@ jobs: release_name: ${{ needs.prepare-release.outputs.release_name }} secrets: inherit + update-winget: + needs: + - prepare-release + - build-and-upload + if: ${{ !inputs.prerelease }} + permissions: + contents: read + uses: ./.github/workflows/update-winget.yml + with: + release_tag: ${{ needs.prepare-release.outputs.tag }} + secrets: inherit + release-ui: needs: prepare-release if: ${{ inputs.release_ui }} diff --git a/.github/workflows/update-winget.yml b/.github/workflows/update-winget.yml index 41b894f6..fd9a656f 100644 --- a/.github/workflows/update-winget.yml +++ b/.github/workflows/update-winget.yml @@ -1,17 +1,83 @@ name: Update Winget on: - release: - types: - - published + workflow_call: + inputs: + release_tag: + description: "Stable release tag to inspect" + required: true + type: string + release_id: + description: "Optional numeric GitHub release id" + required: false + default: "" + type: string + workflow_dispatch: + inputs: + release_tag: + description: "Stable release tag to inspect" + required: true + type: string + release_id: + description: "Optional numeric GitHub release id" + required: false + default: "" + type: string permissions: contents: read jobs: + resolve-release: + name: Resolve release metadata + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.release.outputs.release_id }} + release_tag: ${{ steps.release.outputs.release_tag }} + draft: ${{ steps.release.outputs.draft }} + prerelease: ${{ steps.release.outputs.prerelease }} + steps: + - name: Resolve release metadata + id: release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ inputs.release_tag }} + RELEASE_ID_INPUT: ${{ inputs.release_id }} + run: | + set -euo pipefail + + if [ -n "$RELEASE_ID_INPUT" ]; then + release_api="repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID_INPUT}" + else + release_api="repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" + fi + + release_id="$(gh api "$release_api" --jq '.id')" + release_tag="$(gh api "$release_api" --jq '.tag_name')" + draft="$(gh api "$release_api" --jq '.draft')" + prerelease="$(gh api "$release_api" --jq '.prerelease')" + + if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then + echo "Unable to resolve release id for tag '$RELEASE_TAG'" >&2 + exit 1 + fi + + echo "release_id=$release_id" >> "$GITHUB_OUTPUT" + echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" + echo "draft=$draft" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + + - name: Log resolved release metadata + run: | + echo "Release tag: ${{ steps.release.outputs.release_tag }}" + echo "Release id: ${{ steps.release.outputs.release_id }}" + echo "Draft: ${{ steps.release.outputs.draft }}" + echo "Prerelease: ${{ steps.release.outputs.prerelease }}" + update-winget: name: Submit Winget manifest update - if: ${{ !github.event.release.draft && !github.event.release.prerelease }} + needs: resolve-release + if: ${{ needs.resolve-release.outputs.draft != 'true' && needs.resolve-release.outputs.prerelease != 'true' }} runs-on: ubuntu-latest env: WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER || 'NeuralNomadsAI.CodeNomad' }} @@ -35,8 +101,8 @@ jobs: run: | args=( --repo "${{ github.repository }}" - --release-id "${{ github.event.release.id }}" - --tag "${{ github.event.release.tag_name }}" + --release-id "${{ needs.resolve-release.outputs.release_id }}" + --tag "${{ needs.resolve-release.outputs.release_tag }}" --asset-name-template "$WINGET_WINDOWS_ASSET_NAME_TEMPLATE" --timeout-seconds "$WINGET_ASSET_WAIT_TIMEOUT_SECONDS" --poll-interval-seconds "$WINGET_ASSET_POLL_INTERVAL_SECONDS" @@ -79,7 +145,7 @@ jobs: with: identifier: ${{ env.WINGET_PACKAGE_IDENTIFIER }} version: ${{ steps.release_asset.outputs.version }} - release-tag: ${{ github.event.release.tag_name }} + release-tag: ${{ needs.resolve-release.outputs.release_tag }} installers-regex: ${{ steps.release_asset.outputs.asset_regex }} fork-user: ${{ env.WINGET_FORK_OWNER }} token: ${{ secrets.WINGET_GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 3a8b08cc..395089a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ - When adding new component styles, place them beside their peers in the scoped subdirectory (e.g., `src/styles/messaging/new-part.css`) and import them from the corresponding aggregator file. - Prefer smaller, focused style files (≈150 lines or less) over large monoliths. Split by component or feature area if a file grows beyond that size. - Co-locate reusable UI patterns (buttons, selectors, dropdowns, etc.) under `src/styles/components/` and avoid redefining the same utility classes elsewhere. +- Never use rounded corners in UI styling; keep corners square unless the user explicitly requests otherwise for a specific change. - Document any new styling conventions or directory additions in this file so future changes remain consistent. ## Coding Principles diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..968b5466 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,154 @@ +# Contributing to CodeNomad + +Thank you for your interest in contributing! This guide will help you get started. + +## Prerequisites + +- **Node.js 18+** and npm +- **OpenCode CLI** in your `PATH` (the server connects to the OpenCode binary to manage workspaces) + +## Quick Start + +```bash +git clone https://github.com/NeuralNomadsAI/CodeNomad.git +cd CodeNomad +npm install +npm run dev +``` + +## Finding Issues to Work On + +Browse [open issues](https://github.com/NeuralNomadsAI/CodeNomad/issues) and look for these labels: + +| Label | Meaning | +|---|---| +| `ready-to-work` | Clear scope, ready for anyone to pick up | +| `good-first-issue` | Good for first-time contributors | +| `enhancement` | New feature requests | +| `bug` | Bug reports | + +**Before starting:** comment on the issue so we can discuss approach and avoid duplicate work. + +## Development Workflow + +### 1. Fork and Branch + +```bash +# Fork the repo on GitHub, then clone your fork +git clone https://github.com/YOUR_USERNAME/CodeNomad.git +cd CodeNomad + +# Add the upstream remote +git remote add upstream https://github.com/NeuralNomadsAI/CodeNomad.git + +# Create a branch from upstream/dev +git fetch upstream +git checkout -b fix/your-branch-name upstream/dev +``` + +### 2. Branch Naming + +| Prefix | Use for | +|---|---| +| `fix/` | Bug fixes | +| `feat/` | New features | +| `docs/` | Documentation changes | +| `refactor/` | Code refactoring | +| `chore/` | Build, config, maintenance | + +Examples: `fix/question-queue-ordering`, `feat/retry-tool-call`, `docs/contributing-guide` + +### 3. Make Your Changes + +```bash +# Install dependencies +npm install + +# Run the dev server +npm run dev + +# Run type checking +npm run typecheck --workspace @codenomad/ui +``` + +### 4. Commit + +Write clear, descriptive commit messages. Explain **what** changed and **why**. + +```bash +git add . +git commit -m "fix(ui): preserve question queue order when upserting duplicate requests + +When a question arrives as a global entry and later resolves to a tool +part with a newer timestamp, the original enqueue time was lost, causing +the question to move behind newer entries and break interruption order." +``` + +### 5. Push and Create a PR + +```bash +git push origin your-branch-name +``` + +Then open a pull request on GitHub targeting the `dev` branch. + +**PR checklist:** +- [ ] Branch is based on latest `upstream/dev` +- [ ] One issue per PR (don't mix unrelated changes) +- [ ] Type checking passes: `npm run typecheck` (root) or the workspace-specific script matching your change area +- [ ] Tests pass (if applicable) +- [ ] PR description explains the change, includes relevant screenshots for UI changes, and links related issues when applicable + +## Project Structure + +| Package | Description | +|---|---| +| `packages/server` | Core logic & CLI — workspaces, OpenCode proxy, API, auth | +| `packages/ui` | SolidJS frontend — reactive UI components and stores | +| `packages/electron-app` | Electron desktop shell | +| `packages/tauri-app` | Tauri desktop shell (experimental) | +| `packages/opencode-plugin` | OpenCode plugin integration | +| `packages/cloudflare` | Cloudflare deployment adapters | + +### Key UI Files + +| Path | Purpose | +|---|---| +| `packages/ui/src/stores/session-events.ts` | SSE event handlers (idle, status, permissions, questions) | +| `packages/ui/src/stores/session-actions.ts` | User actions (send message, abort, revert, fork) | +| `packages/ui/src/stores/message-v2/` | Message store (v2 architecture) | +| `packages/ui/src/stores/instances.ts` | Instance management and interruption queues | +| `packages/ui/src/components/tool-call.tsx` | Tool call rendering | +| `packages/ui/src/components/message-block.tsx` | Message display blocks | +| `packages/ui/src/components/session/session-view.tsx` | Main session view | +| `packages/ui/src/lib/i18n/messages/` | Translation files (en, es, fr, ja, ru, he, zh-Hans) | + +> For a comprehensive map of all six functional areas (server, UI, desktop, speech/audio, build, Cloudflare), SDK integration patterns, and feature traces, load the `codenomad-architecture-guide` skill: +> `.opencode/skills/codenomad-architecture-guide/SKILL.md` + +### Styling + +- Tokens: `packages/ui/src/styles/tokens.css` +- Utilities: `packages/ui/src/styles/utilities.css` +- Component styles: `packages/ui/src/styles/components/`, `packages/ui/src/styles/messaging/`, `packages/ui/src/styles/panels/` +- Keep style files under ~150 lines; split by component + +### Internationalization (i18n) + +- Use `useI18n()` in components, `tGlobal()` in stores +- Messages live in `packages/ui/src/lib/i18n/messages//` +- When adding a string: add to `en/` first, then add the same key to every other locale +- Placeholders use `{name}` syntax (word characters only) + +## Code Principles + +- **KISS**: Keep modules narrowly scoped +- **DRY**: Share helpers before copy-pasting +- **Single responsibility**: Split files when concerns diverge +- **Composable primitives**: Prefer signals, hooks, utilities over deep inheritance + +## Need Help? + +- Check existing [issues](https://github.com/NeuralNomadsAI/CodeNomad/issues) and [PRs](https://github.com/NeuralNomadsAI/CodeNomad/pulls) +- Ask in the issue you're working on +- Review the [server documentation](packages/server/README.md) for CLI flags and configuration diff --git a/docs/guides/winget-release-automation.md b/docs/guides/winget-release-automation.md index 63931a15..b61639c0 100644 --- a/docs/guides/winget-release-automation.md +++ b/docs/guides/winget-release-automation.md @@ -1,11 +1,13 @@ # Winget release automation -CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/update-winget.yml`. +CodeNomad publishes Winget updates from the stable GitHub release pipeline. `.github/workflows/reusable-release.yml` now calls `.github/workflows/update-winget.yml` after the release assets finish uploading. ## Trigger -- Runs on `release.published`. -- Exits early for draft or prerelease releases. +- Runs as a reusable workflow from the stable release pipeline, after `build-and-upload` completes. +- Resolves the target release by tag through the GitHub API, then exits early for draft or prerelease releases. +- Can also be rerun manually with `workflow_dispatch` by supplying the stable release tag (and optionally the numeric release id). +- This avoids the old `release.published` trap where a release created by GitHub Actions with the default `GITHUB_TOKEN` does not fan out into a second workflow run. - Waits for the expected stable Windows Tauri asset because the release record can exist before all assets finish uploading. ## Required configuration @@ -28,7 +30,7 @@ CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/ ## Runtime flow -1. Resolve the release version from `github.event.release.tag_name`. +1. Resolve the target release by tag through the GitHub API, then derive the package version from the resolved release tag. 2. Poll the release API until exactly one uploaded asset matches the configured Windows Tauri asset template. 3. Download the matched asset once and compute a SHA-256 for logging and verification. 4. Verify the PAT owner matches `WINGET_FORK_OWNER` and that `${WINGET_FORK_OWNER}/winget-pkgs` is a fork of `microsoft/winget-pkgs`. @@ -38,3 +40,4 @@ CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/ - The workflow does not depend on a persistent local `winget-pkgs` clone. - The current package in `winget-pkgs` uses the release ZIP with a nested NSIS installer, so the workflow targets the stable `CodeNomad-Tauri-windows-x64-{version}.zip` asset. +- If a maintainer publishes a release outside the standard release workflow, they should manually run `Update Winget` for that stable tag. diff --git a/package-lock.json b/package-lock.json index 3e9ea4e2..5d08c142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codenomad-workspace", - "version": "0.17.0", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codenomad-workspace", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "7zip-bin": "^5.2.0", @@ -3266,9 +3266,9 @@ } }, "node_modules/@opencode-ai/sdk": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.16.0.tgz", - "integrity": "sha512-S4H2e9j4rdHs5BQOCjmVEdqdXmKwPFKjXPbPUaWiRJpAjBcZ/uIBpoZkmV+x9BLzc+vrE6WAffMZieQgukt4DA==", + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.8.tgz", + "integrity": "sha512-6MKmsj2ujZyL44jy+12dpwWYDYKPS9fUr+0wVQxaIlPYQ/eAt8T8T3QrybplJ5ZtHfZUX+esXZ02x2UYYm7oEw==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -13409,7 +13409,7 @@ }, "packages/electron-app": { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "yaml": "^2.4.2" @@ -13436,7 +13436,7 @@ }, "packages/opencode-plugin": { "name": "@codenomad/codenomad-opencode-plugin", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "@opencode-ai/plugin": "1.3.7" @@ -13448,7 +13448,7 @@ }, "packages/server": { "name": "@neuralnomads/codenomad", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "@fastify/cors": "^8.5.0", @@ -13490,7 +13490,7 @@ }, "packages/tauri-app": { "name": "@codenomad/tauri-app", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "devDependencies": { "@tauri-apps/cli": "^2.9.4" @@ -13498,12 +13498,12 @@ }, "packages/ui": { "name": "@codenomad/ui", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "@git-diff-view/solid": "^0.0.8", "@kobalte/core": "0.13.11", - "@opencode-ai/sdk": "1.16.0", + "@opencode-ai/sdk": "^1.17.8", "@solidjs/router": "^0.13.0", "@suid/icons-material": "^0.9.0", "@suid/material": "^0.19.0", diff --git a/package.json b/package.json index 55b59c8c..dd823123 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codenomad-workspace", - "version": "0.17.0", + "version": "0.18.0", "private": true, "description": "CodeNomad monorepo workspace", "license": "MIT", diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index dca634be..8bb58526 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.17.0", + "version": "0.18.0", "description": "CodeNomad - AI coding assistant", "license": "MIT", "author": { diff --git a/packages/opencode-plugin/package.json b/packages/opencode-plugin/package.json index dbfd1a83..b2193719 100644 --- a/packages/opencode-plugin/package.json +++ b/packages/opencode-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/codenomad-opencode-plugin", - "version": "0.17.0", + "version": "0.18.0", "private": true, "license": "MIT", "type": "module", diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json index dd351117..eff79de6 100644 --- a/packages/server/package-lock.json +++ b/packages/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neuralnomads/codenomad", - "version": "0.17.0", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neuralnomads/codenomad", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "@fastify/cors": "^11.2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 3f931ab6..15228764 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad", - "version": "0.17.0", + "version": "0.18.0", "description": "CodeNomad Server", "license": "MIT", "author": { diff --git a/packages/tauri-app/Cargo.lock b/packages/tauri-app/Cargo.lock index 3cfaf27b..52242aad 100644 --- a/packages/tauri-app/Cargo.lock +++ b/packages/tauri-app/Cargo.lock @@ -497,7 +497,7 @@ dependencies = [ [[package]] name = "codenomad-tauri" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json index ba8771c2..64645e0a 100644 --- a/packages/tauri-app/package.json +++ b/packages/tauri-app/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/tauri-app", - "version": "0.17.0", + "version": "0.18.0", "private": true, "license": "MIT", "scripts": { diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index 4443392a..c94d775c 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codenomad-tauri" -version = "0.17.0" +version = "0.18.0" edition = "2021" license = "MIT" diff --git a/packages/tauri-app/src-tauri/tauri.conf.json b/packages/tauri-app/src-tauri/tauri.conf.json index a99e1082..243cc80a 100644 --- a/packages/tauri-app/src-tauri/tauri.conf.json +++ b/packages/tauri-app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodeNomad", - "version": "0.17.0", + "version": "0.18.0", "identifier": "ai.neuralnomads.codenomad.client", "build": { "beforeDevCommand": "npm run dev:bootstrap", diff --git a/packages/ui/package.json b/packages/ui/package.json index bc2c83b2..8c365390 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/ui", - "version": "0.17.0", + "version": "0.18.0", "private": true, "license": "MIT", "type": "module", @@ -13,7 +13,7 @@ "dependencies": { "@git-diff-view/solid": "^0.0.8", "@kobalte/core": "0.13.11", - "@opencode-ai/sdk": "1.16.0", + "@opencode-ai/sdk": "^1.17.8", "@solidjs/router": "^0.13.0", "@suid/icons-material": "^0.9.0", "@suid/material": "^0.19.0", diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 259eff7c..866a896b 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -47,7 +47,6 @@ import { clearActiveParentSession, createSession, fetchSessions, - getSessionFetchLimit, updateSessionAgent, updateSessionModel, } from "./stores/sessions" @@ -389,7 +388,7 @@ const App: Component = () => { if (!confirmed) return - await stopInstance(instanceId) + stopInstance(instanceId) } async function handleNewSession(instanceId: string) { @@ -419,7 +418,7 @@ const App: Component = () => { clearActiveParentSession(instanceId) try { - await fetchSessions(instanceId, { reset: true, limit: getSessionFetchLimit(instanceId) }) + await fetchSessions(instanceId, { reset: true }) } catch (error) { log.error("Failed to refresh sessions after closing", error) } diff --git a/packages/ui/src/components/action-overflow-menu.tsx b/packages/ui/src/components/action-overflow-menu.tsx index a66a25f3..6ee5a5eb 100644 --- a/packages/ui/src/components/action-overflow-menu.tsx +++ b/packages/ui/src/components/action-overflow-menu.tsx @@ -7,6 +7,7 @@ export interface ActionOverflowMenuItem { label: string icon?: JSXElement disabled?: boolean + checked?: boolean destructive?: boolean onSelect: () => void | Promise onMouseEnter?: () => void @@ -52,6 +53,8 @@ export default function ActionOverflowMenu(props: ActionOverflowMenuProps) { { if (item.disabled) return diff --git a/packages/ui/src/components/context-meter.tsx b/packages/ui/src/components/context-meter.tsx index cd375269..b0643949 100644 --- a/packages/ui/src/components/context-meter.tsx +++ b/packages/ui/src/components/context-meter.tsx @@ -7,6 +7,7 @@ interface ContextMeterProps { usedLabel: string availableLabel: string class?: string + centerValue?: boolean } const LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted" @@ -104,18 +105,32 @@ export const ContextMeter: Component = (props) => { const tooltipText = () => `Context Used: ${percentLabel()}` + const valuePill = () => ( +
+ {props.usedLabel} + {props.formatTokens(used())} + / + {props.availableLabel} + + {available() !== null ? props.formatTokens(available() as number) : "--"} + +
+ ) + + if (props.centerValue) { + return ( +
+
{circle()}
+ {valuePill()} +
+ ) + } + return (
{circle()} -
- {props.usedLabel} - {props.formatTokens(used())} - / - {props.availableLabel} - - {available() !== null ? props.formatTokens(available() as number) : "--"} - -
+ {valuePill()}
) } diff --git a/packages/ui/src/components/instance-tabs.tsx b/packages/ui/src/components/instance-tabs.tsx index d751a7e0..54866708 100644 --- a/packages/ui/src/components/instance-tabs.tsx +++ b/packages/ui/src/components/instance-tabs.tsx @@ -1,4 +1,4 @@ -import { Component, For, Show, createMemo, createSignal } from "solid-js" +import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js" import { Dynamic } from "solid-js/web" import { DragDropProvider, @@ -37,15 +37,9 @@ interface SortableAppTabProps { onClose: (tabId: string) => void } -const SortableAppTab: Component = (props) => { - const sortable = createSortable(props.tab.id) - +const AppTabContent: Component = (props) => { return ( -
+ <> {props.tab.kind === "instance" ? ( = (props) => {
)} + + ) +} + +const SortableAppTab: Component = (props) => { + const sortable = createSortable(props.tab.id) + + return ( +
+
) } +const StaticAppTab: Component = (props) => { + return ( +
+ +
+ ) +} + +const isTouchOnlyPointer = () => { + if (typeof window === "undefined") return false + return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches && !window.matchMedia?.("(any-pointer: fine)")?.matches) +} + const InstanceTabs: Component = (props) => { const { t } = useI18n() const { preferences } = useConfig() const tabIds = createMemo(() => props.tabs.map((tab) => tab.id)) + const [dragReorderEnabled, setDragReorderEnabled] = createSignal(!isTouchOnlyPointer()) + + onMount(() => { + if (typeof window === "undefined") return + const coarseQuery = window.matchMedia?.("(pointer: coarse)") + const fineQuery = window.matchMedia?.("(any-pointer: fine)") + if (!coarseQuery || !fineQuery) return + + const syncDragReorder = () => setDragReorderEnabled(!isTouchOnlyPointer()) + syncDragReorder() + coarseQuery.addEventListener("change", syncDragReorder) + fineQuery.addEventListener("change", syncDragReorder) + + onCleanup(() => { + coarseQuery.removeEventListener("change", syncDragReorder) + fineQuery.removeEventListener("change", syncDragReorder) + }) + }) /** Whether to show toast history panel */ const [showToastHistory, setShowToastHistory] = createSignal(false) @@ -132,22 +171,38 @@ const InstanceTabs: Component = (props) => {
- - - - - {(tab) => ( - - )} - - - - + + {(tab) => ( + + )} + + } + > + + + + + {(tab) => ( + + )} + + + + +
1}> diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index c0ad2ad6..cf3e8ab4 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -30,6 +30,7 @@ import PermissionApprovalModal from "../permission-approval-modal" import SessionView from "../session/session-view" import MessageSection from "../message-section" import PromptAttachmentsBar from "../prompt-input/PromptAttachmentsBar" +import ActionOverflowMenu, { type ActionOverflowMenuItem } from "../action-overflow-menu" import { formatTokenTotal } from "../../lib/formatters" import ContextMeter from "../context-meter" import { sseManager } from "../../lib/sse-manager" @@ -156,8 +157,10 @@ const InstanceShell2: Component = (props) => { }) const isPhoneLayout = createMemo(() => layoutMode() === "phone") - const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery()) + const narrowHeaderLayout = createMemo(() => sessionCenterWidthStep() === "narrow") + const compactHeaderLayout = createMemo(() => narrowHeaderLayout() || compactHeaderQuery()) const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout()) + const showCompactFullscreenButton = createMemo(() => isPhoneLayout() && !props.mobileFullscreenMode) const compactPromptLayout = createMemo(() => layoutMode() !== "desktop") const leftPinningSupported = createMemo(() => layoutMode() !== "phone") const rightPinningSupported = createMemo(() => layoutMode() !== "phone") @@ -539,6 +542,24 @@ const InstanceShell2: Component = (props) => { window.dispatchEvent(new CustomEvent(OPEN_SESSION_SEARCH_EVENT)) } + const narrowHeaderMenuItems = createMemo(() => { + const PreviewIcon = PreviewToggleIcon() + return [ + { + key: "search", + label: t("instanceShell.chatSearch.openAriaLabel"), + icon: