diff --git a/.docs/architecture.md b/.docs/architecture.md index 15d80d8131d..bb7d2c22141 100644 --- a/.docs/architecture.md +++ b/.docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -CUT3 ships as a shared web app plus an optional Electron desktop shell, backed by a Node.js server that exposes HTTP/WebSocket APIs and routes work to provider-specific runtimes. +Rowl ships as a shared web app plus an optional Electron desktop shell, backed by a Node.js server that exposes HTTP/WebSocket APIs and routes work to provider-specific runtimes. ``` ┌──────────────────────────────┐ @@ -33,4 +33,4 @@ CUT3 ships as a shared web app plus an optional Electron desktop shell, backed b - `apps/server` serves the built UI, validates WebSocket requests, owns orchestration, and routes provider-native work through the provider layer. - Codex uses `codex app-server` over JSON-RPC stdio. - GitHub Copilot, OpenCode, and Kimi Code use ACP-backed runtime managers. -- Pi uses the embedded `@mariozechner/pi-coding-agent` Node SDK while CUT3 keeps Pi packages, resource discovery, and system-prompt discovery disabled. +- Pi uses the embedded `@mariozechner/pi-coding-agent` Node SDK while Rowl keeps Pi packages, resource discovery, and system-prompt discovery disabled. diff --git a/.docs/ci.md b/.docs/ci.md index ad2dd11f3e8..c12282f6639 100644 --- a/.docs/ci.md +++ b/.docs/ci.md @@ -6,7 +6,7 @@ - `.github/workflows/release.yml` still builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. - The release workflow preflight reruns the browser suite plus the Linux desktop build/smoke path before the per-platform packaging matrix starts, aligns package versions to the release tag, then archives and uploads the prebuilt `apps/desktop/dist-electron` and `apps/server/dist` bundle so each packaging job can extract it and reuse `--skip-build` instead of rebuilding the JS pipeline four times. - Release jobs now cache Bun/Turbo and Electron downloads, upload binary artifacts with `compression-level: 0`, and publish a `SHA256SUMS` manifest after verifying the downloaded release assets. -- The release workflow still auto-enables signing when Apple or Azure Trusted Signing secrets are present, and it can now be hardened to fail macOS/Windows releases if signing secrets are missing by setting `CUT3_REQUIRE_SIGNING=true` or using the `require_signing` workflow-dispatch input. +- The release workflow still auto-enables signing when Apple or Azure Trusted Signing secrets are present, and it can now be hardened to fail macOS/Windows releases if signing secrets are missing by setting `ROWL_REQUIRE_SIGNING=true` or using the `require_signing` workflow-dispatch input. - The release workflow also supports a non-mutating `workflow_dispatch` dry run via `dry_run=true`; that path builds and validates the full release asset set, uploads a workflow artifact, and skips both GitHub Release publishing and the final version-bump push to `main`. -- CLI npm publishing is optional and no longer blocks GitHub Releases; enable it with the `publish_cli` workflow-dispatch input or the `CUT3_PUBLISH_CLI=true` repository variable. +- CLI npm publishing is optional and no longer blocks GitHub Releases; enable it with the `publish_cli` workflow-dispatch input or the `ROWL_PUBLISH_CLI=true` repository variable. - See `docs/release.md` for the full release/signing/checksum checklist. diff --git a/.docs/encyclopedia.md b/.docs/encyclopedia.md index c6d9c0b391f..118486386d1 100644 --- a/.docs/encyclopedia.md +++ b/.docs/encyclopedia.md @@ -1,6 +1,6 @@ # Encyclopedia -This is a living glossary for CUT3. It explains what common terms mean in this codebase. +This is a living glossary for Rowl. It explains what common terms mean in this codebase. ## Table of contents @@ -29,7 +29,7 @@ A Git worktree used as an isolated workspace for a thread. If a thread has a `wo #### Repo-local skill -A repo-owned instruction artifact discovered from `.cut3/skills//SKILL.md` and attachable per turn from the composer. Each skill must declare `name` and `description`, and `name` must match the lowercase hyphenated directory name. See [projectWorkspaceMetadata.ts][32] and [ComposerSkillPicker.tsx][33]. +A repo-owned instruction artifact discovered from `.rowl/skills//SKILL.md` and attachable per turn from the composer. Each skill must declare `name` and `description`, and `name` must match the lowercase hyphenated directory name. See [projectWorkspaceMetadata.ts][32] and [ComposerSkillPicker.tsx][33]. ### Thread timeline diff --git a/.docs/provider-architecture.md b/.docs/provider-architecture.md index f5f013bf13d..32fbd61cc99 100644 --- a/.docs/provider-architecture.md +++ b/.docs/provider-architecture.md @@ -16,7 +16,7 @@ Current push channels include: Request bodies cover more than provider lifecycle calls. The WebSocket surface currently includes: - orchestration commands and diff/snapshot queries -- project registry search/write operations, including workspace `AGENTS.md` discovery/drafting, `.cut3/commands/*.md` template discovery, and `.cut3/skills//SKILL.md` discovery +- project registry search/write operations, including workspace `AGENTS.md` discovery/drafting, `.rowl/commands/*.md` template discovery, and `.rowl/skills//SKILL.md` discovery - thread collaboration and history operations, including share create/get/revoke/import, compaction, undo, redo, and redo-status queries - shell/editor integration - git operations @@ -29,16 +29,16 @@ Provider-native runtime details are hidden behind the server provider layer: - **GitHub Copilot**: ACP-backed runtime sessions - **OpenCode**: ACP-backed runtime sessions through `opencode acp` - **Kimi Code**: ACP-backed runtime sessions, with optional API-key-backed startup -- **Pi**: embedded `@mariozechner/pi-coding-agent` Node SDK sessions with CUT3-owned approval gating and Pi resource discovery disabled +- **Pi**: embedded `@mariozechner/pi-coding-agent` Node SDK sessions with Rowl-owned approval gating and Pi resource discovery disabled Unexpected provider exits are reduced into orchestration session state as stopped sessions that still preserve the runtime exit reason in `thread.session.lastError`, so crashes do not render as silent clean stops in the UI. When a thread resolves to a workspace root and that workspace contains `AGENTS.md`, the server-side provider reactor wraps each outgoing provider turn with the contents of that file before dispatching the turn to the active provider runtime. This keeps workspace instructions provider-agnostic instead of relying on a provider-specific session bootstrap mechanism. -Pi is the main exception to the repo's usual external-CLI pattern: CUT3 embeds Pi through its Node SDK, reuses Pi auth/models config from `~/.pi/agent`, and keeps Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes disabled so Pi threads do not double-apply repo instructions that CUT3 already injects. +Pi is the main exception to the repo's usual external-CLI pattern: Rowl embeds Pi through its Node SDK, reuses Pi auth/models config from `~/.pi/agent`, and keeps Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes disabled so Pi threads do not double-apply repo instructions that Rowl already injects. Codex, GitHub Copilot, OpenCode, Kimi Code, and Pi are the currently implemented providers. Gemini is a visible coming-soon entry in the picker, and `claudeCode` plus `cursor` remain unavailable placeholders for future support. -In the current OpenCode phase, CUT3 still treats credential storage and OAuth flows as provider-owned concerns, but it now inspects OpenCode's resolved config paths plus `opencode auth list`, `opencode mcp list`, and `opencode mcp auth list` to surface provider credentials and MCP status in both Settings and `server.getConfig`. CUT3 still launches `opencode acp`, consumes its ACP model/session events, and applies per-session runtime-mode overrides through `OPENCODE_CONFIG_CONTENT` rather than proxying the underlying auth flows itself. +In the current OpenCode phase, Rowl still treats credential storage and OAuth flows as provider-owned concerns, but it now inspects OpenCode's resolved config paths plus `opencode auth list`, `opencode mcp list`, and `opencode mcp auth list` to surface provider credentials and MCP status in both Settings and `server.getConfig`. Rowl still launches `opencode acp`, consumes its ACP model/session events, and applies per-session runtime-mode overrides through `OPENCODE_CONFIG_CONTENT` rather than proxying the underlying auth flows itself. For the researched GLM Coding Plan and MiniMax roadmap, see [./glm-minimax-support-plan.md](./glm-minimax-support-plan.md). diff --git a/.docs/provider-settings.md b/.docs/provider-settings.md index b6d2dab4cc6..e00e0aca8d0 100644 --- a/.docs/provider-settings.md +++ b/.docs/provider-settings.md @@ -1,6 +1,6 @@ # Provider settings -CUT3 stores app settings locally on the current device. Open **Settings** in the app to manage appearance, provider-specific paths, model options, thread defaults, and response defaults. +Rowl stores app settings locally on the current device. Open **Settings** in the app to manage appearance, provider-specific paths, model options, thread defaults, and response defaults. ## Appearance @@ -39,18 +39,18 @@ The **Providers** section supports local overrides for each provider runtime: - Custom binary path - **OpenCode** - Custom binary path - - OpenCode account authentication stays in OpenCode itself via `opencode auth login` and `opencode auth logout`; CUT3 does not store those credentials in this phase + - OpenCode account authentication stays in OpenCode itself via `opencode auth login` and `opencode auth logout`; Rowl does not store those credentials in this phase - MCP server auth/debug remains server-specific in OpenCode via commands such as `opencode mcp auth ` and `opencode mcp debug ` - - The OpenCode settings panel inspects `opencode auth list`, `opencode mcp list`, `opencode mcp auth list`, and the resolved OpenCode config paths so users can see current provider credentials, MCP connectivity, and copyable auth/debug commands without leaving CUT3 - - When the top-level CUT3 OpenRouter key is set, new OpenCode sessions also inherit it as `OPENROUTER_API_KEY` so OpenCode provider configs can reference it through `{env:OPENROUTER_API_KEY}` + - The OpenCode settings panel inspects `opencode auth list`, `opencode mcp list`, `opencode mcp auth list`, and the resolved OpenCode config paths so users can see current provider credentials, MCP connectivity, and copyable auth/debug commands without leaving Rowl + - When the top-level Rowl OpenRouter key is set, new OpenCode sessions also inherit it as `OPENROUTER_API_KEY` so OpenCode provider configs can reference it through `{env:OPENROUTER_API_KEY}` - **Kimi Code** - Custom binary path - Optional API key stored locally and injected into new Kimi CLI sessions - Without an API key, authenticate in Kimi Code CLI itself with `kimi login` or by starting `kimi` and running `/login` - **Pi** - - No separate binary override in CUT3; Pi is embedded through `@mariozechner/pi-coding-agent` + - No separate binary override in Rowl; Pi is embedded through `@mariozechner/pi-coding-agent` - Pi auth and model discovery still come from `~/.pi/agent` (`auth.json`, `models.json`, Pi env vars, or the external `pi` / `/login` flow) - - CUT3 intentionally disables Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes on this path so workspace instructions still come only from CUT3 + - Rowl intentionally disables Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes on this path so workspace instructions still come only from Rowl Leave a binary field blank to use the provider executable from your `PATH`. @@ -64,7 +64,7 @@ Settings also keeps a few cross-provider behavior defaults: - **Thread sharing mode** - `Manual`: create share links only when you explicitly choose `/share` or the thread action - `Auto`: create a share link automatically after a new server-backed thread settles for the first time - - `Disabled`: block creation of new share links from CUT3 until you change the setting again + - `Disabled`: block creation of new share links from Rowl until you change the setting again - **Stream assistant messages** - Show token-by-token output while a turn is in progress - **Show tool details** @@ -93,19 +93,19 @@ This is an app-level default. It applies when starting new Codex turns from the ### OpenRouter free models -CUT3 now shows OpenRouter free models in their own settings card and their own top-level section inside the model picker. +Rowl now shows OpenRouter free models in their own settings card and their own top-level section inside the model picker. -- CUT3 always includes the built-in `openrouter/free` router. -- The settings page fetches OpenRouter's live model catalog, but CUT3 only lists models that are explicitly free-locked (`openrouter/free` or `:free`) and advertise the full native tool-calling surface CUT3 needs (`tools` plus `tool_choice`). +- Rowl always includes the built-in `openrouter/free` router. +- The settings page fetches OpenRouter's live model catalog, but Rowl only lists models that are explicitly free-locked (`openrouter/free` or `:free`) and advertise the full native tool-calling surface Rowl needs (`tools` plus `tool_choice`). - You can pin any listed OpenRouter free model into the picker and `/model` suggestions with one click. -- If the live catalog cannot be fetched, CUT3 surfaces that state in Settings instead of silently hiding it. -- CUT3 now keeps a last-known-good compatible catalog locally, so the picker and the Settings card can continue showing the previous free-model list with a stale/offline warning instead of collapsing back to only the router entry. -- If a pinned OpenRouter `:free` model cannot be served because the route is unavailable, overloaded, rate-limited, or filtered out by provider/privacy constraints, CUT3 automatically retries the turn through `openrouter/free` and shows a warning banner so the turn does not silently drift onto a billed model. CUT3 does not auto-retry Responses API validation failures or payment/credit errors, because those need explicit user action instead of a silent reroute. +- If the live catalog cannot be fetched, Rowl surfaces that state in Settings instead of silently hiding it. +- Rowl now keeps a last-known-good compatible catalog locally, so the picker and the Settings card can continue showing the previous free-model list with a stale/offline warning instead of collapsing back to only the router entry. +- If a pinned OpenRouter `:free` model cannot be served because the route is unavailable, overloaded, rate-limited, or filtered out by provider/privacy constraints, Rowl automatically retries the turn through `openrouter/free` and shows a warning banner so the turn does not silently drift onto a billed model. Rowl does not auto-retry Responses API validation failures or payment/credit errors, because those need explicit user action instead of a silent reroute. - OpenRouter free models still depend on OpenRouter account limits. New accounts only get a small free allowance, purchased credits raise the daily free-model limit, and negative balances can still produce `402 Payment Required` even for `openrouter/free`. ### Custom model slugs -CUT3 supports saved custom model ids for: +Rowl supports saved custom model ids for: - **GitHub Copilot** - **OpenCode** provider/model ids such as `z-ai/glm-4.5` or `minimax/MiniMax-M2.7` @@ -114,7 +114,7 @@ CUT3 supports saved custom model ids for: - Additional Codex model ids you want to save manually - Additional OpenRouter `:free` model ids from the current live catalog -OpenCode also advertises runtime-discovered models through ACP. CUT3 merges those live models into the picker after an OpenCode session starts, and keeps a built-in `Default` option under OpenCode so a first session can start without CUT3 guessing a vendor-specific `provider/model` id. Pi now exposes authenticated provider/model ids from local `~/.pi/agent` auth/models state directly in the picker and `/model` suggestions before the first Pi turn, while still keeping `pi/default` available as a compatibility fallback for threads or settings that intentionally rely on Pi choosing its own default provider/model. +OpenCode also advertises runtime-discovered models through ACP. Rowl merges those live models into the picker after an OpenCode session starts, and keeps a built-in `Default` option under OpenCode so a first session can start without Rowl guessing a vendor-specific `provider/model` id. Pi now exposes authenticated provider/model ids from local `~/.pi/agent` auth/models state directly in the picker and `/model` suggestions before the first Pi turn, while still keeping `pi/default` available as a compatibility fallback for threads or settings that intentionally rely on Pi choosing its own default provider/model. Saved custom model ids appear in: @@ -123,7 +123,7 @@ Saved custom model ids appear in: The app normalizes entries before saving them, ignores built-in duplicates, and refuses OpenRouter slugs that are not explicit free variants. -If you add an OpenRouter API key in Settings, CUT3 launches Codex with per-session OpenRouter overrides whenever you pick `openrouter/free` or another saved OpenRouter `:free` slug such as `google/gemma-3n-e4b-it:free`. Native Codex models still use your normal Codex authentication. +If you add an OpenRouter API key in Settings, Rowl launches Codex with per-session OpenRouter overrides whenever you pick `openrouter/free` or another saved OpenRouter `:free` slug such as `google/gemma-3n-e4b-it:free`. Native Codex models still use your normal Codex authentication. ### Chat picker controls @@ -131,7 +131,7 @@ The chat composer now exposes a richer model picker instead of only nested provi - The picker is searchable across provider names, model labels, and raw model slugs. - Models are grouped by provider, with OpenRouter kept as its own top-level section. -- `Provider readiness` opens an in-chat onboarding surface that summarizes local provider health, groups providers into ready / attention / unavailable states, offers copyable login commands where CUT3 knows the real CLI flow, lets you jump straight into OpenRouter/Kimi key entry, and links back to Settings for deeper runtime configuration. +- `Provider readiness` opens an in-chat onboarding surface that summarizes local provider health, groups providers into ready / attention / unavailable states, offers copyable login commands where Rowl knows the real CLI flow, lets you jump straight into OpenRouter/Kimi key entry, and links back to Settings for deeper runtime configuration. - `Manage models` opens an in-chat model management surface with per-model visibility toggles plus favorite pinning. - Favorites stay pinned near the top of the picker, and recent model selections are also surfaced ahead of the long tail so switching providers or models takes fewer searches. - Hidden models are removed from both the main picker and `/model` suggestions, but they stay saved locally so you can restore them later with `Show all`. @@ -146,9 +146,9 @@ The composer exposes provider-aware turn controls. - **GitHub Copilot**: provider-supported reasoning values from the live ACP session, currently including `Extra High` on recent Copilot CLI builds when the selected model exposes it - **OpenCode**: no reasoning-effort picker is shown - **Kimi Code**: no reasoning-effort picker is shown -- **Pi**: reasoning-capable Pi models now expose Pi thinking levels in the composer (`Default`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`). CUT3 reads Pi's live model reasoning flag from the authenticated Pi catalog, then uses the embedded Pi SDK session to apply and clamp the selected level against the active model's capabilities. +- **Pi**: reasoning-capable Pi models now expose Pi thinking levels in the composer (`Default`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`). Rowl reads Pi's live model reasoning flag from the authenticated Pi catalog, then uses the embedded Pi SDK session to apply and clamp the selected level against the active model's capabilities. -Reasoning choices are scoped by provider. CUT3 still shows a Reasoning badge for OpenRouter models that advertise reasoning support, but it does not expose Codex-style reasoning-effort levels for OpenRouter models because the OpenRouter catalog does not currently describe which effort values are valid per model. +Reasoning choices are scoped by provider. Rowl still shows a Reasoning badge for OpenRouter models that advertise reasoning support, but it does not expose Codex-style reasoning-effort levels for OpenRouter models because the OpenRouter catalog does not currently describe which effort values are valid per model. ### Codex fast mode @@ -161,7 +161,7 @@ Codex also has a per-turn `Fast Mode` toggle in the composer controls. This is s ### Context window UI -CUT3 hides the "token context left" UI for OpenRouter-routed models because the routed model can change and the remaining-context display is not reliable enough there. +Rowl hides the "token context left" UI for OpenRouter-routed models because the routed model can change and the remaining-context display is not reliable enough there. ### Usage dashboard @@ -169,33 +169,33 @@ The composer context ring is now a full `Usage dashboard` trigger instead of onl - Click the context ring to open a dialog with the current selection's documented/live context window, token breakdown, latest matching runtime snapshot metadata, and latest reported spend when the provider exposes it. - The model picker footer also includes a `Usage` shortcut so you can open the same dashboard while browsing providers/models. -- If the latest stored runtime snapshot belongs to a different provider/model than the current selection, CUT3 calls that out explicitly instead of silently showing stale numbers. +- If the latest stored runtime snapshot belongs to a different provider/model than the current selection, Rowl calls that out explicitly instead of silently showing stale numbers. - GitHub Copilot selections also include current premium-request quota information in the dashboard. -- Cost reporting depends on the provider runtime. CUT3 shows the latest reported USD amount when the provider supplies it, otherwise the spend card stays unavailable instead of guessing. +- Cost reporting depends on the provider runtime. Rowl shows the latest reported USD amount when the provider supplies it, otherwise the spend card stays unavailable instead of guessing. ### Image attachments The composer also supports lightweight image input: - attach images with the paperclip button, drag-and-drop, or paste -- CUT3 accepts image files only +- Rowl accepts image files only - each message can include up to `8` images - each image is limited to `10MB` - attached images render as inline previews in the composer and thread timeline -- when a message only includes images, CUT3 sends a small bootstrap prompt so providers still receive a valid user turn +- when a message only includes images, Rowl sends a small bootstrap prompt so providers still receive a valid user turn - thread export/bootstrap text includes attachment names and metadata instead of re-embedding image bytes ### Workspace instructions and command templates The composer now surfaces repo-owned workspace behavior directly: -- CUT3 ships built-in slash commands for common thread actions, including `/new`, `/compact`, `/share`, `/unshare`, `/undo`, `/redo`, `/export`, `/details`, `/init`, `/plan`, `/default`, `/model`, and `/mcp` when the active provider supports MCP. -- CUT3 checks the active workspace root for `AGENTS.md` and shows whether it is currently available. +- Rowl ships built-in slash commands for common thread actions, including `/new`, `/compact`, `/share`, `/unshare`, `/undo`, `/redo`, `/export`, `/details`, `/init`, `/plan`, `/default`, `/model`, and `/mcp` when the active provider supports MCP. +- Rowl checks the active workspace root for `AGENTS.md` and shows whether it is currently available. - `/init` drafts or updates `AGENTS.md` using the current workspace shape, then saves it through the same guarded project-write path used by other workspace actions. -- CUT3 loads repo-local slash-command templates from `.cut3/commands/*.md`. +- Rowl loads repo-local slash-command templates from `.rowl/commands/*.md`. - Template frontmatter supports `description`, optional `provider`, optional `model`, optional `interactionMode`, optional `runtimeMode`, and optional `sendImmediately`. - Template bodies can interpolate `$ARGUMENTS` and `$1` through `$9`. -- When `sendImmediately: true` is set, CUT3 expands the template and dispatches the turn directly. Otherwise it expands into the composer for review before sending. +- When `sendImmediately: true` is set, Rowl expands the template and dispatches the turn directly. Otherwise it expands into the composer for review before sending. ### Follow-up queueing and steering @@ -209,7 +209,7 @@ When a turn is already running, the composer exposes follow-up controls instead ### Repo-local skills -CUT3 also discovers repo-local skills from `.cut3/skills//SKILL.md`. +Rowl also discovers repo-local skills from `.rowl/skills//SKILL.md`. - The directory name and frontmatter `name` must match and use the same lowercase hyphenated skill-name format. - `SKILL.md` frontmatter must include string `name` and `description` fields. @@ -229,7 +229,7 @@ Settings now includes a `Permission policies` section for durable approval rules ### OpenCode MCP visibility -CUT3 now inspects OpenCode MCP state through `opencode mcp list` and `opencode mcp auth list`, exposes those entries in `server.getConfig`, and shows the resolved OpenCode config sources in Settings. That inspection keeps disabled, auth-gated, failed, and connected OpenCode MCP entries separated so the composer `/mcp` browser and the Settings panel reflect the active OpenCode provider state instead of collapsing everything into a single generic list. OpenCode still owns the actual OAuth flow and credential storage, so CUT3 only reports status and offers copyable CLI commands such as `opencode mcp auth ` and `opencode mcp debug `. +Rowl now inspects OpenCode MCP state through `opencode mcp list` and `opencode mcp auth list`, exposes those entries in `server.getConfig`, and shows the resolved OpenCode config sources in Settings. That inspection keeps disabled, auth-gated, failed, and connected OpenCode MCP entries separated so the composer `/mcp` browser and the Settings panel reflect the active OpenCode provider state instead of collapsing everything into a single generic list. OpenCode still owns the actual OAuth flow and credential storage, so Rowl only reports status and offers copyable CLI commands such as `opencode mcp auth ` and `opencode mcp debug `. ## Related docs diff --git a/.docs/quick-start.md b/.docs/quick-start.md index e8790418c6f..86278143810 100644 --- a/.docs/quick-start.md +++ b/.docs/quick-start.md @@ -8,7 +8,7 @@ bun run dev bun run dev:desktop # Desktop development on an isolated port set -CUT3_DEV_INSTANCE=feature-xyz bun run dev:desktop +ROWL_DEV_INSTANCE=feature-xyz bun run dev:desktop # Production bun run build @@ -27,7 +27,7 @@ bun run dist:desktop:linux bun run dist:desktop:win # Or from any project directory after publishing: -bunx cut3 +bunx rowl ``` ## Local desktop release builds @@ -46,13 +46,13 @@ Use the matching host OS when possible. Cross-platform packaging is not the defa ## After startup -- Open **Settings** to configure appearance, including theme presets, per-mode palette/font controls, an English/Persian language switch, and an optional chat background image, plus provider binary overrides, OpenRouter and Kimi API keys, OpenCode binary selection, Pi guidance, model preferences, thread sharing mode (`Manual`, `Auto`, `Disabled`), and whether tool/work-log entries stay visible in the main timeline. If you use Kimi without an API key, authenticate in Kimi Code CLI with `kimi login` or the in-shell `/login` flow. If you want to use Pi, authenticate it outside CUT3 through the Pi CLI (`pi` or `bunx pi`) and `/login`, or populate `~/.pi/agent/auth.json` / provider env vars first. -- Use **Settings > Permission policies** to save app-wide or project-scoped approval rules when you want CUT3 to automatically `allow`, `ask`, or `deny` repeated approval requests. -- Use the **OpenRouter Free Models** card in Settings to review the current OpenRouter catalog entries that are both free-locked and CUT3-compatible, then pin any of them into the picker. If the next live refresh fails, CUT3 now falls back to the last known-good catalog and labels it as stale instead of collapsing the list unexpectedly. -- Save extra GitHub Copilot, OpenCode, Kimi, Pi provider/model ids, custom Codex ids, or currently listed OpenRouter `:free` model ids if you want them in the picker and `/model` suggestions. You can also pin favorites in `Manage models`, and CUT3 now keeps recent picks near the top of the picker so repeated model switches take fewer steps. -- For Codex, choose a default service tier in Settings, use the top-level OpenRouter section in the model picker when you want `openrouter/free` or another current free OpenRouter model, and adjust reasoning / `Fast Mode` per turn from the composer. OpenRouter models can advertise reasoning support, but CUT3 does not expose Codex-specific reasoning-effort levels for them. Pi reasoning-capable models now also expose Pi thinking levels from the composer while leaving Pi's own default thinking untouched until you choose an override. If CUT3 has to retry a pinned OpenRouter free model through `openrouter/free`, the chat shows a warning banner instead of switching silently. -- Put repo-local skills in `.cut3/skills//SKILL.md` with `name` and `description` frontmatter, then select them from the composer Skills picker before sending a turn. -- Use the paperclip button, drag-and-drop, or paste to attach up to 8 images per message. CUT3 accepts image files only and limits each image to 10 MB. +- Open **Settings** to configure appearance, including theme presets, per-mode palette/font controls, an English/Persian language switch, and an optional chat background image, plus provider binary overrides, OpenRouter and Kimi API keys, OpenCode binary selection, Pi guidance, model preferences, thread sharing mode (`Manual`, `Auto`, `Disabled`), and whether tool/work-log entries stay visible in the main timeline. If you use Kimi without an API key, authenticate in Kimi Code CLI with `kimi login` or the in-shell `/login` flow. If you want to use Pi, authenticate it outside Rowl through the Pi CLI (`pi` or `bunx pi`) and `/login`, or populate `~/.pi/agent/auth.json` / provider env vars first. +- Use **Settings > Permission policies** to save app-wide or project-scoped approval rules when you want Rowl to automatically `allow`, `ask`, or `deny` repeated approval requests. +- Use the **OpenRouter Free Models** card in Settings to review the current OpenRouter catalog entries that are both free-locked and Rowl-compatible, then pin any of them into the picker. If the next live refresh fails, Rowl now falls back to the last known-good catalog and labels it as stale instead of collapsing the list unexpectedly. +- Save extra GitHub Copilot, OpenCode, Kimi, Pi provider/model ids, custom Codex ids, or currently listed OpenRouter `:free` model ids if you want them in the picker and `/model` suggestions. You can also pin favorites in `Manage models`, and Rowl now keeps recent picks near the top of the picker so repeated model switches take fewer steps. +- For Codex, choose a default service tier in Settings, use the top-level OpenRouter section in the model picker when you want `openrouter/free` or another current free OpenRouter model, and adjust reasoning / `Fast Mode` per turn from the composer. OpenRouter models can advertise reasoning support, but Rowl does not expose Codex-specific reasoning-effort levels for them. Pi reasoning-capable models now also expose Pi thinking levels from the composer while leaving Pi's own default thinking untouched until you choose an override. If Rowl has to retry a pinned OpenRouter free model through `openrouter/free`, the chat shows a warning banner instead of switching silently. +- Put repo-local skills in `.rowl/skills//SKILL.md` with `name` and `description` frontmatter, then select them from the composer Skills picker before sending a turn. +- Use the paperclip button, drag-and-drop, or paste to attach up to 8 images per message. Rowl accepts image files only and limits each image to 10 MB. - On a fresh install with no configured projects, the empty chat view now guides you through adding your first project folder and opens the first draft thread automatically. - Pick `Full access` or `Supervised` in the toolbar depending on whether you want direct execution or approval-gated actions. - Switch between `Chat` and `Plan` when you want plan-first collaboration with the plan sidebar. @@ -61,6 +61,6 @@ Use the matching host OS when possible. Cross-platform packaging is not the defa - Use the thread header `Undo` and `Redo` controls, or the matching slash commands, to move through recent restore snapshots after destructive changes. - Use `Fork thread here` on a message to branch from that point, and use the diff panel to fork from a completed checkpoint. - Use the sidebar search box plus the `Active`, `All`, and `Archived` filters to find threads quickly. Projects and threads can be pinned or archived locally, projects can switch between recent and manual ordering, and each project shows the 10 most recent matching threads before you expand the rest. -- When a provider emits task lifecycle events, CUT3 shows a compact task panel above the conversation so you can track active and completed tasks without mixing them into the curated work log. +- When a provider emits task lifecycle events, Rowl shows a compact task panel above the conversation so you can track active and completed tasks without mixing them into the curated work log. See [provider-settings.md](provider-settings.md) for the current settings surface and [runtime-modes.md](runtime-modes.md) for the execution controls. diff --git a/.docs/runtime-modes.md b/.docs/runtime-modes.md index bad32a1cffa..706e0cdab1e 100644 --- a/.docs/runtime-modes.md +++ b/.docs/runtime-modes.md @@ -1,20 +1,20 @@ # Runtime modes -CUT3 has a global runtime mode switch in the chat toolbar: +Rowl has a global runtime mode switch in the chat toolbar: - **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. - **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. -Runtime mode sets the default sandbox and approval posture for a new session. Persistent permission policies from Settings can still auto-allow, ask, or deny specific requests after the provider raises an approval. Pi is the one notable difference here: CUT3 still gates Pi tools through the same approval UX in `Supervised`, but Pi does not add a separate OS sandbox beyond its own embedded tool execution. +Runtime mode sets the default sandbox and approval posture for a new session. Persistent permission policies from Settings can still auto-allow, ask, or deny specific requests after the provider raises an approval. Pi is the one notable difference here: Rowl still gates Pi tools through the same approval UX in `Supervised`, but Pi does not add a separate OS sandbox beyond its own embedded tool execution. ## Interaction modes The chat toolbar also has an interaction-mode toggle: - **Chat**: the normal execution mode. -- **Plan**: switches the provider into plan-first collaboration so the assistant focuses on exploration, clarification, and producing a detailed plan instead of directly executing the work. For Pi, CUT3 enforces a read-only Pi tool set plus explicit plan instructions because Pi does not expose a separate native plan-mode protocol. +- **Plan**: switches the provider into plan-first collaboration so the assistant focuses on exploration, clarification, and producing a detailed plan instead of directly executing the work. For Pi, Rowl enforces a read-only Pi tool set plus explicit plan instructions because Pi does not expose a separate native plan-mode protocol. -When a plan is active, CUT3 can also show a **plan sidebar** so the current plan stays visible while you continue the conversation. +When a plan is active, Rowl can also show a **plan sidebar** so the current plan stays visible while you continue the conversation. The plan sidebar also supports: @@ -42,4 +42,4 @@ Settings also controls how new share links behave: - **Manual**: create share links only when you explicitly choose `/share` or the thread action. - **Auto**: create a share link automatically after a new server-backed thread settles for the first time. -- **Disabled**: block creation of new share links from CUT3 until you change the setting again. +- **Disabled**: block creation of new share links from Rowl until you change the setting again. diff --git a/.docs/scripts.md b/.docs/scripts.md index 3e6b5315a63..761a9ad460f 100644 --- a/.docs/scripts.md +++ b/.docs/scripts.md @@ -3,11 +3,11 @@ - `bun run dev` — Starts contracts, server, and web in `turbo watch` mode. - `bun run dev:server` — Starts just the WebSocket server (uses Bun TypeScript execution). - `bun run dev:web` — Starts just the Vite dev server for the web app. -- Dev commands default `CUT3_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state. -- Separate `bun run dev:web` and `bun run dev:server` launches reuse one shared port offset per `CUT3_STATE_DIR`, so they stay on the same `377x` / `573x` pair instead of drifting apart. +- Dev commands default `ROWL_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state. +- Separate `bun run dev:web` and `bun run dev:server` launches reuse one shared port offset per `ROWL_STATE_DIR`, so they stay on the same `377x` / `573x` pair instead of drifting apart. - Override server CLI-equivalent flags from root dev commands with `--`, for example: `bun run dev -- --state-dir ~/.t3/another-dev-state` -- If you bind the web server off loopback, set `CUT3_AUTH_TOKEN`; unauthenticated off-box WebSocket clients are rejected. +- If you bind the web server off loopback, set `ROWL_AUTH_TOKEN`; unauthenticated off-box WebSocket clients are rejected. - `bun run start` — Runs the production server (serves built web app as static files). - `bun run build` — Builds contracts, web app, and server through Turbo. - `bun run typecheck` — Strict TypeScript checks for all packages. @@ -25,7 +25,7 @@ - Default local builds are unsigned and not notarized. - The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. -- Desktop production windows load the bundled UI from `cut3://app/index.html` (not a `127.0.0.1` document URL). +- Desktop production windows load the bundled UI from `rowl://app/index.html` (not a `127.0.0.1` document URL). - Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an auth token for WebSocket/API traffic. - Your tester can still open an unsigned macOS build by right-clicking the app and choosing **Open** on first launch. - To keep staging files for debugging package contents, run: `bun run dist:desktop:dmg -- --keep-stage` @@ -41,16 +41,16 @@ ## Desktop smoke test guarantee - `bun run test:desktop-smoke` exercises launch-time desktop integration only. -- The smoke test passes only after Electron reaches the bundled backend readiness marker (`[cut3-desktop-ready]{...}`) and no obvious startup exception appears in stdout or stderr. +- The smoke test passes only after Electron reaches the bundled backend readiness marker (`[rowl-desktop-ready]{...}`) and no obvious startup exception appears in stdout or stderr. - The smoke test does not validate full renderer interaction, menu flows, or update installation. - For the architecture and logging details behind that contract, see `apps/desktop/README.md`. ## Running multiple dev instances -Set `CUT3_DEV_INSTANCE` to any value to deterministically shift all dev ports together. +Set `ROWL_DEV_INSTANCE` to any value to deterministically shift all dev ports together. - Default ports: server `3773`, web `5733` -- Shifted ports: `base + offset` (offset is hashed from `CUT3_DEV_INSTANCE`) -- Example: `CUT3_DEV_INSTANCE=branch-a bun run dev:desktop` +- Shifted ports: `base + offset` (offset is hashed from `ROWL_DEV_INSTANCE`) +- Example: `ROWL_DEV_INSTANCE=branch-a bun run dev:desktop` -If you want full control instead of hashing, set `CUT3_PORT_OFFSET` to a numeric offset. +If you want full control instead of hashing, set `ROWL_PORT_OFFSET` to a numeric offset. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75678a7e2f9..3183d6414d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,8 +82,8 @@ jobs: require_signing="${{ github.event.inputs.require_signing }}" dry_run="${{ github.event.inputs.dry_run }}" else - publish_cli="${{ vars.CUT3_PUBLISH_CLI || 'false' }}" - require_signing="${{ vars.CUT3_REQUIRE_SIGNING || 'false' }}" + publish_cli="${{ vars.ROWL_PUBLISH_CLI || 'false' }}" + require_signing="${{ vars.ROWL_REQUIRE_SIGNING || 'false' }}" dry_run="false" fi @@ -478,7 +478,7 @@ jobs: run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - name: Build CLI package - run: bun run build --filter=@t3tools/web --filter=cut3 + run: bun run build --filter=@t3tools/web --filter=rowl - name: Publish CLI package run: node apps/server/scripts/cli.ts publish --tag latest --app-version "${{ needs.preflight.outputs.version }}" --verbose diff --git a/AGENTS.md b/AGENTS.md index b53ceda2082..6179bf0c351 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# CLAUDE.md +# Rowl Agent Guidance ## Task Completion Requirements @@ -8,136 +8,32 @@ - If a change affects user-visible behavior, settings, build steps, release steps, or developer workflows, update the relevant documentation in the same change as part of the implementation. - Treat stale docs as a bug. A task is not complete until `README.md`, `CONTRIBUTING.md`, `.docs/*`, `docs/*`, and any task-specific guides touched by the change are accurate. - Update `AGENTS.md` too when a task exposes a repeatable mistake, workflow correction, or durable lesson that should guide future work. -- Keep developer docs aligned with the current `CUT3_*` dev-runner env names. `dev:web` and `dev:server` are expected to share one port offset per `CUT3_STATE_DIR`, so docs should not describe them as independently drifting port selections. +- Keep developer docs aligned with the current `ROWL_*` dev-runner env names. `dev:web` and `dev:server` are expected to share one port offset per `ROWL_STATE_DIR`, so docs should not describe them as independently drifting port selections. - Keep `apps/web/vitest.browser.config.ts` explicitly prebundling `vitest/browser` and `vitest-browser-react`; cold-cache browser runs can otherwise fail before tests start because the optimized browser bundle imports raw Vitest browser helpers. -- Keep Linux Electron smoke runs CI-safe: GitHub-hosted Linux runners need the smoke harness to add `--no-sandbox`, or Electron exits before CUT3 can emit the desktop backend ready marker. -- Keep release hardening aligned across workflow + docs: reuse the preflight desktop bundle with `dist:desktop:artifact -- --skip-build` instead of rebuilding the JS pipeline in every packaging job, always publish/verify `SHA256SUMS`, gate signed macOS/Windows releases through the `CUT3_REQUIRE_SIGNING` policy instead of silently shipping unsigned artifacts when signing is expected, and keep manual `dry_run` releases non-mutating by skipping both GitHub Release publishing and the finalize version-bump push to `main`. -- Keep provider availability claims in docs and onboarding copy aligned with `apps/web/src/session-logic.ts` and its tests. -- Keep built-in composer slash command parsing, aliases, and menu suggestions aligned through `apps/web/src/composer-logic.ts`; do not duplicate the command list in multiple places and let them drift. -- Keep chat timeline rendering consolidated in `apps/web/src/components/chat/MessagesTimeline.tsx`; do not reintroduce an inline `MessagesTimeline` copy inside `apps/web/src/components/ChatView.tsx`. -- Keep OpenCode auth UX aligned with the real CLI surface: credentials are managed via `opencode auth login/logout`, while CUT3 only inspects OpenCode state and forwards the shared OpenRouter key to new OpenCode sessions as `OPENROUTER_API_KEY` when configured. -- Keep OpenCode MCP status parsing aligned with the real `opencode mcp list` / `opencode mcp auth list` output. Disabled, auth-gated, failed, and connected entries should stay distinguishable in CUT3 instead of being collapsed into a generic success/failure view. -- Keep approval UX labels precise: approval decision `cancel` only dismisses/cancels the pending approval prompt, not the running turn, so UI copy must never present it as a turn stop/interrupt action. -- Keep browser composer interrupt/approval controls optimistic and visibly pending until orchestration state catches up; stop/approve/decline clicks should produce immediate browser feedback instead of looking inert while websocket/provider roundtrips finish, and browser interrupt requests should resolve the best current turn id from session/latest activity instead of assuming a single field is current. -- Keep Kimi auth UX aligned with the official Kimi CLI docs: user-facing guidance should mention `kimi login` and the in-shell `/login` path, plus the CUT3 Kimi API key setting, instead of assuming only one auth flow. -- Keep Pi auth and discovery UX aligned with the real Pi surfaces: CUT3 embeds Pi through the Node SDK, but Pi credentials still live in `~/.pi/agent/auth.json` / Pi env vars or the external `pi` `/login` flow, and CUT3 should keep Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes disabled so CUT3 injects workspace instructions only once. -- Keep Pi model discovery live in the chat UI: `server.getConfig` / provider refreshes must rerun provider health, and when Pi already exposes authenticated models from local `~/.pi/agent` state, the picker and `/model` suggestions should surface those provider/model ids instead of collapsing back to a static `pi/default` placeholder. -- Keep provider onboarding/readiness UX centralized in the chat provider-readiness surface plus shared provider health/state helpers; do not scatter provider-specific setup copy, login commands, or readiness summaries across multiple unrelated components. -- Keep model-picker ranking state centralized through app settings + shared model-preference helpers. Favorites, recents, hidden-model state, picker ordering, and `/model` suggestion ordering must stay aligned instead of each view inventing its own ranking rules. -- Keep Pi reasoning UX aligned with the real Pi SDK surface: CUT3 should trust Pi's live `model.reasoning` catalog flag plus `AgentSession.getAvailableThinkingLevels()` / `setThinkingLevel()` instead of hardcoding Codex-style assumptions, and should preserve Pi defaults until the user explicitly picks a thinking level override. -- Keep GitHub Copilot reasoning UX aligned with the real CLI/ACP surface: current Copilot CLI builds expose `xhigh` reasoning for some models, so contracts, probes, and composer docs must not clamp Copilot to only low/medium/high. -- Keep GitHub Copilot model slugs aligned with live ACP session metadata. Do not hard-code blanket slug rewrites; mirror whatever the runtime actually advertises, and treat stale picker-only entries that never appear in live Copilot/OpenCode model catalogs as bugs. -- When retiring built-in provider model slugs from picker catalogs, preserve legacy thread hydration and provider inference for historical snapshots/imports; removing a picker entry must not silently reclassify old provider threads or rewrite their stored model ids. -- Keep server-side fallback models aligned with `DEFAULT_MODEL_BY_PROVIDER` in `packages/contracts/src/model.ts`; do not hardcode older Codex defaults in bootstraps, managers, or internal helpers. -- Do not leave ad-hoc provider `console.log` debugging in runtime managers; provider/account payloads can leak into server logs. -- Keep provider event logging opt-in. Raw provider prompts, tool payloads, approval answers, and runtime output must not be persisted by default; use `CUT3_ENABLE_PROVIDER_EVENT_LOGS=1` only for deliberate local debugging. -- Keep provider exit failures visible end-to-end. If a runtime emits `session.exited` with a non-graceful reason, orchestration must preserve that reason in `thread.session.lastError` so OpenCode/Copilot/Kimi/Codex crashes do not look like silent clean stops. -- When testing hot orchestration streams backed by PubSub, avoid `fork + sleep` subscription races. Start the collector with an explicit readiness handshake (for example `Effect.forkScoped` plus `Effect.yieldNow`, or another deterministic subscription barrier) before dispatching commands. -- Keep interactive controls properly disabled during in-flight async operations (e.g. export, share, revoke): users must not be able to trigger conflicting actions while a prior action is still completing. Guard format toggles, download buttons, and secondary actions behind the relevant `isSaving`/`isRevoking` flags. -- Keep sidebar organization logic centralized. Pin/archive/search/filter/sort behavior should stay in shared helpers/stores (`apps/web/src/components/Sidebar.logic.ts`, `apps/web/src/lib/threadOrdering.ts`, `apps/web/src/sidebarPreferencesStore.ts`) instead of being reimplemented ad hoc inside multiple sidebar render branches. -- Keep project-creation and first-run onboarding flows centralized through the shared project-creation hook/component path. Empty-state onboarding and the sidebar add-project affordance should reuse the same project-create + first-thread navigation logic instead of drifting into separate implementations. -- Normalize workspace-path comparisons in the shared project-creation flow before deciding whether a project already exists. Trailing slashes, slash-direction differences, and Windows drive-letter casing should not create duplicate projects or break the “focus existing project” path. -- Do not swallow first-thread navigation failures after creating a project. If project creation succeeds but opening the initial draft fails, surface that error so the UI does not pretend setup finished cleanly. -- Keep new-thread draft-context normalization centralized. When reusing an existing draft in `Local` mode, explicitly clear inherited branch/worktree state instead of silently reopening a stale worktree-backed draft. -- Keep chat follow-up queue state centralized in `apps/web/src/threadSendQueue.ts` instead of duplicating per-thread queue bookkeeping inside individual composer controls. -- Keep ARIA semantics aligned with visual affordances: disclosure/expand buttons need `aria-expanded`, toggle-style buttons need `aria-pressed` or `role="radio"` with `aria-checked`, icon-only buttons need explicit `aria-label`, tree-like file lists need `role="tree"`, and controls revealed only on hover (e.g. terminal close buttons) must also be revealed on `focus-visible` so keyboard users can reach them. -- Keep `aria-label` values on interactive groups and their trigger buttons accurate and descriptive of the actual feature. Do not leave placeholder labels from copy-paste (e.g. "Subscription actions" for an editor picker, "Copy options" for an editor menu). -- When a button visually looks disabled (opacity, cursor-not-allowed), make it actually `disabled` so it is removed from tab order and does not fire click handlers. CSS-only faux-disabled states are a keyboard trap. - -## Project Snapshot - -CUT3 is a minimal web GUI for using coding agents. It currently supports Codex, GitHub Copilot, OpenCode, Kimi Code, and the Pi agent harness, with a visible Gemini coming-soon entry plus unavailable picker placeholders for Claude Code and Cursor. - -This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. +- Keep Linux Electron smoke runs CI-safe: GitHub-hosted Linux runners need the smoke harness to add `--no-sandbox`, or Electron exits before Rowl can emit the desktop backend ready marker. +- Keep release hardening aligned across workflow + docs: reuse the preflight desktop bundle with `dist:desktop:artifact -- --skip-build` instead of rebuilding the JS pipeline in every packaging job, always publish/verify `SHA256SUMS`, gate signed macOS/Windows releases through the `ROWL_REQUIRE_SIGNING` policy instead of silently shipping unsigned artifacts when signing is expected, and keep manual `dry_run` releases non-mutating by skipping both GitHub Release publishing and the finalize version-bump push to `main`. -## Core Priorities +## Provider Availability -1. Performance first. -2. Reliability first. -3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams). +Keep provider availability claims in docs and onboarding copy aligned with `apps/web/src/session-logic.ts` and its tests. -If a tradeoff is required, choose correctness and robustness over short-term convenience. +Current providers: -## GPT-5.4 Prompt Guidance +- **Available**: Codex, OpenRouter, GitHub Copilot, Kimi Code, OpenCode, Pi +- **Coming soon**: Gemini +- **Unavailable placeholders**: Claude Code, Cursor - -You are working in the CUT3 monorepo. -Priorities: +## Core Priorities 1. Performance first. 2. Reliability first. -3. Predictable behavior under load, reconnects, and partial streams. - Architecture: - -- apps/server: provider sessions, orchestration, websocket server -- apps/web: React/Vite UI and session UX -- apps/desktop: Electron shell and desktop-native integrations -- packages/contracts: schemas/contracts only -- packages/shared: shared runtime utilities - Do not make schema-only packages carry runtime logic. - Prefer shared extraction over duplicated local fixes. - - - - -- Use tools whenever they materially improve correctness, completeness, or grounding. -- Use subagents proactively for bounded exploration, parallel read-only work, or other delegable tasks that keep the main context window clear; prefer sub agents for easier bounded tasks such as repo scans, doc audits, and other low-risk side work. -- Do not stop early when another inspection, search, or validation step would materially improve the result. -- Keep going until the task is complete and verification passes. -- If a lookup or test result is partial or suspiciously narrow, retry with a different strategy. - - - - -- Before editing, inspect the relevant code paths and contracts. -- If you do not know something, research it first. Do not assume runtime behavior, APIs, library semantics, or repository conventions. -- Check source code and documentation before making implementation claims. Do not over-hallucinate or smooth over uncertainty. -- Do not skip prerequisite discovery just because the final change seems obvious. -- Resolve upstream/downstream dependencies before mutating code. - - - - -- Treat the task as incomplete until all requested deliverables are handled or explicitly marked blocked. -- Keep an internal checklist of affected runtime paths, UI paths, contracts, and tests. -- Do not leave empty TODOs or placeholder follow-ups in fixes/features unless the user explicitly asked for staged work and the blocker is documented clearly. -- If something is blocked, state exactly what is missing. - - - -Before finalizing: - -- Check correctness against the user request. -- Check grounding against the codebase and tool outputs. -- Check formatting and repo conventions. -- Check whether tests/typecheck/lint relevant to the change should run. -- Check whether any docs are now stale and update them before finishing. -- Keep documentation current while working, not as an afterthought. Tracking reality in docs is part of the implementation. -- If the task included easy bounded work that could be delegated safely, prefer a GPT-5.4 Mini subagent for that side work and keep final synthesis, risky edits, and verification in the main agent. -- For localization changes, keep the settings schema, document `lang`/`dir`, and locale-aware date/time formatting aligned. Do not ship a language toggle that only changes labels. -- For mixed-language surfaces, do not flip the whole app shell to RTL just because one locale is RTL. Keep untranslated/shared shells LTR and scope RTL to the views that are actually localized, or English truncation and control ordering will regress. -- Check that async UI controls expose a visible loading state and actionable error recovery, not only a disabled state. -- For desktop startup, packaging, or release-flow changes, keep `apps/desktop/README.md`, `.docs/scripts.md`, `docs/release.md`, and the desktop smoke test aligned on what is actually guaranteed. - - - - -- If required context is missing, do not guess. -- Prefer repo inspection first. -- Be direct and realistic about uncertainty, failures, and tradeoffs. Do not sugarcoat problems or overstate confidence. -- Ask only the minimal clarifying question when the answer cannot be derived locally. - - - - -- For implementation tasks: make the change, verify it, then summarize outcome and risks briefly. -- For review tasks: list findings first with file/line references. -- For no-change/planning tasks: provide the exact files and settings that would need changes. - +3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams). + +If a tradeoff is required, choose correctness and robustness over short-term convenience. ## Maintainability -Long term maintainability is a core priority. If you add new functionality, first check if there are shared logic that can be extracted to a separate module. Duplicate logic across mulitple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem. +Long term maintainability is a core priority. If you add new functionality, first check if there are shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem. ## Package Roles @@ -147,9 +43,9 @@ Long term maintainability is a core priority. If you add new functionality, firs - `packages/contracts`: Shared Effect Schema schemas and TypeScript contracts for provider events, WebSocket protocol, keybindings, and model/session types. Keep this package schema-only — no runtime logic. - `packages/shared`: Shared runtime utilities consumed by both server and web. Uses explicit subpath exports (e.g. `@t3tools/shared/git`) — no barrel index. -## Provider Runtimes (Important) +## Provider Runtimes -CUT3 exposes one orchestration/WebSocket surface, then delegates provider-native runtime behavior to provider adapters and managers. +Rowl exposes one orchestration/WebSocket surface, then delegates provider-native runtime behavior to provider adapters and managers. How we use it in this codebase: @@ -157,21 +53,26 @@ How we use it in this codebase: - GitHub Copilot sessions are brokered through ACP-backed runtime management in `apps/server/src/copilotAcpManager.ts`. - OpenCode sessions are brokered through ACP-backed runtime management in `apps/server/src/opencodeAcpManager.ts`. - Kimi Code sessions are brokered through ACP-backed runtime management in `apps/server/src/kimiAcpManager.ts`, including optional API-key-backed startup. -- Pi sessions are brokered through the embedded `@mariozechner/pi-coding-agent` Node SDK in `apps/server/src/piSdkManager.ts`, while CUT3 intentionally disables Pi's own resource discovery so repo instructions still come only from CUT3. +- Pi sessions are brokered through the embedded `@mariozechner/pi-coding-agent` Node SDK in `apps/server/src/piSdkManager.ts`, while Rowl intentionally disables Pi's own resource discovery so repo instructions still come only from Rowl. - Cross-provider routing and shared runtime event fan-out are coordinated in `apps/server/src/provider/Layers/ProviderService.ts`. - WebSocket request handling and push channels are served from `apps/server/src/wsServer.ts`. - The web app consumes orchestration domain events plus terminal/server push channels over WebSocket. -- For future tool-backed providers, prefer runtimes with a native app-server or ACP surface instead of inventing a bespoke terminal wrapper. This matters for plan-backed products like GLM Coding Plan, where direct API adapters do not satisfy the vendor's supported-tool quota rules. -- OpenCode's `opencode acp` is the current best-fit substrate for new plan-backed integrations because it matches CUT3's existing ACP provider pattern. -Docs: +For future tool-backed providers, prefer runtimes with a native app-server or ACP surface instead of inventing a bespoke terminal wrapper. OpenCode's `opencode acp` is the current best-fit substrate for new plan-backed integrations because it matches Rowl's existing ACP provider pattern. -- Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server +## Key Environment Variables + +- `ROWL_AUTH_TOKEN`: WebSocket auth token +- `ROWL_STATE_DIR`: State directory (default: `~/.t3/rowl`) +- `ROWL_MODE`: Runtime mode (`desktop`, `web`) +- `ROWL_PORT`: Server port +- `ROWL_DEV_INSTANCE`: Multiple dev instances (shifts ports) +- `ROWL_PORT_OFFSET`: Numeric port offset +- `ROWL_REQUIRE_SIGNING`: Require signing for releases +- `ROWL_ENABLE_PROVIDER_EVENT_LOGS`: Enable provider event logging ## Reference Repos - Open-source Codex repo: https://github.com/openai/codex - Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor - Pi mono repo (`packages/coding-agent`): https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent - -Use these as implementation references when designing protocol handling, UX flows, and operational safeguards. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26a0d008cd2..d7374a8ca1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,66 +8,38 @@ You can still open an issue or PR, but please do so knowing there is a high chan If that sounds annoying, that is because it is. This project is still early and we are trying to keep scope, quality, and direction under control. -PRs are automatically labeled with a `vouch:*` trust status and a `size:*` diff size based on changed lines. - -If you are an external contributor, expect `vouch:unvouched` until we explicitly add you to [.github/VOUCHED.td](.github/VOUCHED.td). - ## What We Are Most Likely To Accept -Small, focused bug fixes. - -Small reliability fixes. - -Small performance improvements. - -Tightly scoped maintenance work that clearly improves the project without changing its direction. +- Small, focused bug fixes +- Small reliability fixes +- Small performance improvements +- Tightly scoped maintenance work that clearly improves the project without changing its direction ## What We Are Least Likely To Accept -Large PRs. +- Large PRs +- Drive-by feature work +- Opinionated rewrites +- Anything that expands product scope without us asking for it first -Drive-by feature work. - -Opinionated rewrites. - -Anything that expands product scope without us asking for it first. - -If you open a 1,000+ line PR full of new features, we will probably close it quickly and remember that you ignored the clearly written instructions. +If you open a 1,000+ line PR full of new features, we will probably close it quickly. ## If You Still Want To Open A PR -Keep it small. - -Explain exactly what changed. - -Explain exactly why the change should exist. - -Do not mix unrelated fixes together. - -If the PR makes anything resembling a UI change, include clear before/after images. - -If the change depends on motion, timing, transitions, or interaction details, include a short video. - -If we have to guess what changed, we are much less likely to review it. +1. Keep it small +2. Explain exactly what changed and why +3. Do not mix unrelated fixes together +4. If the PR makes UI changes, include before/after images +5. If the change depends on motion, timing, or transitions, include a short video ## Documentation And Agent Hygiene -Treat stale docs as a bug. - -If your change affects behavior, settings, build steps, release steps, or contributor workflows, update the relevant docs in the same PR. That includes `README.md`, `AGENTS.md`, `CONTRIBUTING.md`, `.docs/*`, `docs/*`, and any task-specific guide touched by the change. - -If you use coding agents, delegate easy bounded work like repo scans, read-only checks, and doc audits to sub agents when that keeps the main review and edit loop clearer. Keep risky edits, final synthesis, and verification owned by the primary agent or author. +Treat stale docs as a bug. If your change affects behavior, settings, build steps, release steps, or contributor workflows, update the relevant docs in the same PR. ## Issues First -If you are thinking about a non-trivial change, open an issue first. - -That still does not mean we will want the PR, but it gives you a chance to avoid wasting your time. +If you are thinking about a non-trivial change, open an issue first. That doesn't mean we will want the PR, but it gives you a chance to avoid wasting your time. ## Be Realistic -Opening a PR does not create an obligation on our side. - -We may close it. We may ignore it. We may ask you to shrink it. We may reimplement the idea ourselves later. - -If you are fine with that, proceed. +Opening a PR does not create an obligation on our side. We may close it, ignore it, ask you to shrink it, or reimplement the idea ourselves later. diff --git a/KEYBINDINGS.md b/KEYBINDINGS.md index 91e4207b550..a4ee95dcde9 100644 --- a/KEYBINDINGS.md +++ b/KEYBINDINGS.md @@ -1,6 +1,6 @@ # Keybindings -CUT3 reads keybindings from the active state directory: +Rowl reads keybindings from the active state directory: - `/keybindings.json` @@ -8,7 +8,7 @@ Examples: - Default server/desktop path: `~/.t3/userdata/keybindings.json` - Root dev commands usually use a dev-scoped state dir, so the file is typically under `~/.t3/dev/keybindings.json` -- `--state-dir` or `CUT3_STATE_DIR` changes the location +- `--state-dir` or `ROWL_STATE_DIR` changes the location The file must be a JSON array of rules: @@ -25,6 +25,7 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`]( ```json [ + { "key": "mod+b", "command": "sidebar.toggle", "when": "!terminalFocus" }, { "key": "mod+j", "command": "terminal.toggle" }, { "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" }, { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" }, @@ -33,6 +34,8 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`]( { "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" }, { "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" }, { "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" }, + { "key": "escape", "command": "chat.interrupt", "when": "!terminalFocus" }, + { "key": "mod+k", "command": "commandPalette.toggle" }, { "key": "mod+o", "command": "editor.openFavorite" } ] ``` @@ -53,6 +56,7 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged ### Available Commands +- `sidebar.toggle`: show/hide the projects and threads sidebar - `terminal.toggle`: open/close terminal drawer - `terminal.split`: split terminal (in focused terminal context by default) - `terminal.new`: create new terminal (in focused terminal context by default) @@ -60,6 +64,8 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged - `diff.toggle`: open/close the current diff panel outside terminal focus - `chat.new`: create a new chat thread preserving the active thread's branch/worktree state - `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`)) +- `chat.interrupt`: interrupt the active chat session +- `commandPalette.toggle`: open/close the command palette - `editor.openFavorite`: open current project/worktree in the last-used editor - `script.{id}.run`: run a project script by id (for example `script.test.run`) diff --git a/README.md b/README.md index c97b515d03e..415ec464ae2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -# CUT3 +# Rowl

- CUT3 + Rowl

-CUT3 is a minimal web GUI for coding agents. It currently supports Codex, GitHub Copilot, OpenCode, Kimi Code, and the Pi agent harness. The picker also shows Gemini as coming soon, while Claude Code and Cursor remain unavailable placeholders. +Rowl is a minimal web GUI for coding agents. It currently supports Codex, GitHub Copilot, OpenCode, Kimi Code, and the Pi agent harness. The picker also shows Gemini as coming soon, while Claude Code and Cursor remain unavailable placeholders. + +> **Note**: Rowl is based on [CUT3](https://github.com/yappologistic/t3code). This is a fork with custom integrations and features. ## Screenshot -![CUT3 screenshot](./CUT3.png) +![Rowl screenshot](./CUT3.png) ## Supported providers @@ -23,7 +25,7 @@ Gemini is intentionally shown as coming soon in the provider picker. Claude Code ## How to use > [!WARNING] -> Install at least one supported provider runtime before starting CUT3. Codex, GitHub Copilot, OpenCode, and Kimi Code still depend on their native CLIs plus whatever auth or API keys they require. Pi is embedded directly in CUT3, but it still needs Pi auth/config under `~/.pi/agent` (or the equivalent Pi environment variables) before Pi-backed sessions can start: +> Install at least one supported provider runtime before starting Rowl. Codex, GitHub Copilot, OpenCode, and Kimi Code still depend on their native CLIs plus whatever auth or API keys they require. Pi is embedded directly in Rowl, but it still needs Pi auth/config under `~/.pi/agent` (or the equivalent Pi environment variables) before Pi-backed sessions can start: > > - [Codex CLI](https://github.com/openai/codex) > - [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent) @@ -35,37 +37,37 @@ Gemini is intentionally shown as coming soon in the provider picker. Claude Code bun run start ``` -If you bind CUT3 to a non-loopback host, set `CUT3_AUTH_TOKEN`. Off-box WebSocket clients are rejected unless they present that token. +If you bind Rowl to a non-loopback host, set `ROWL_AUTH_TOKEN`. Off-box WebSocket clients are rejected unless they present that token. Published npm package: ```bash -bunx cut3 +bunx rowl ``` You can also just install the desktop app. It's cooler. If this fork does not currently publish desktop releases, build one locally with the commands below. -Published CUT3 builds are listed on the [CUT3 Releases page](https://github.com/yappologistic/t3code/releases). +For the full local packaging and release notes, see [docs/release.md](docs/release.md) and [.docs/scripts.md](.docs/scripts.md). -Once the app is running, choose Codex, GitHub Copilot, OpenCode, Kimi Code, or Pi from the provider picker before starting a session. If this is your first run and CUT3 does not know any projects yet, the empty chat view now walks you through adding a project folder and immediately opens the first draft thread for it. +Once the app is running, choose Codex, GitHub Copilot, OpenCode, Kimi Code, or Pi from the provider picker before starting a session. If this is your first run and Rowl does not know any projects yet, the empty chat view now walks you through adding a project folder and immediately opens the first draft thread for it. ## Workspace instructions and slash commands -CUT3 now recognizes three repo-owned workspace surfaces: +Rowl now recognizes three repo-owned workspace surfaces: -- `AGENTS.md` at the workspace root. When it exists, CUT3 wraps every new provider turn with those workspace instructions on the server side. -- `.cut3/commands/*.md` for repo-local slash-command templates. -- `.cut3/skills//SKILL.md` for repo-local skills that can be attached per turn from the composer. +- `AGENTS.md` at the workspace root. When it exists, Rowl wraps every new provider turn with those workspace instructions on the server side. +- `.rowl/commands/*.md` for repo-local slash-command templates. +- `.rowl/skills//SKILL.md` for repo-local skills that can be attached per turn from the composer. From the composer: - Run built-in slash commands such as `/new` (`/clear`), `/compact` (`/summarize`), `/share`, `/unshare`, `/undo`, `/redo`, `/export`, `/details`, `/init`, `/plan`, `/default`, `/model`, and `/mcp` (when the active provider supports MCP). -- Type `/` to see those built-in commands plus any templates discovered from `.cut3/commands/*.md`. -- Open the Skills picker to attach repo-local skills discovered from `.cut3/skills//SKILL.md`. Skill files must include `name` and `description` frontmatter, and `name` must match the lowercase hyphenated directory name. -- Attach up to **8 images per message** with the paperclip button, drag-and-drop, or paste. CUT3 accepts image files only, enforces a **10 MB per image** limit, shows inline previews in the composer and thread timeline, and includes attachment names in bootstrap/export summaries. -- When a turn is already running, use the composer follow-up controls to **Queue** the next message or **Steer** the run so CUT3 interrupts the current turn and sends your new follow-up next. Press `Enter` to use the current Queue/Steer mode, or `Cmd/Ctrl+Enter` to use the opposite mode for that one follow-up. +- Type `/` to see those built-in commands plus any templates discovered from `.rowl/commands/*.md`. +- Open the Skills picker to attach repo-local skills discovered from `.rowl/skills//SKILL.md`. Skill files must include `name` and `description` frontmatter, and `name` must match the lowercase hyphenated directory name. +- Attach up to **8 images per message** with the paperclip button, drag-and-drop, or paste. Rowl accepts image files only, enforces a **10 MB per image** limit, shows inline previews in the composer and thread timeline, and includes attachment names in bootstrap/export summaries. +- When a turn is already running, use the composer follow-up controls to **Queue** the next message or **Steer** the run so Rowl interrupts the current turn and sends your new follow-up next. Press `Enter` to use the current Queue/Steer mode, or `Cmd/Ctrl+Enter` to use the opposite mode for that one follow-up. - Template frontmatter can set `description`, optional `provider`, optional `model`, optional `interactionMode`, optional `runtimeMode`, and optional `sendImmediately`. Template bodies support `$ARGUMENTS` plus positional placeholders `$1` through `$9`. @@ -99,27 +101,25 @@ Use the matching host OS when possible: - Build Linux artifacts on Linux. - Build Windows artifacts on Windows. -For the full local packaging and release notes, see [docs/release.md](docs/release.md) and [.docs/scripts.md](.docs/scripts.md). - ## Provider settings and model controls Open Settings in the app to configure provider-specific behavior on the current device. - **Appearance**: choose the base light/dark/system mode, switch to integrated presets like Lilac, and configure a custom chat background image with adjustable fade and blur. - **Language**: switch the settings experience and shared app shell between English and Persian. Persian also flips document direction and locale-aware time/date formatting in the web UI. -- **Provider overrides**: set custom binary paths for Codex, Copilot, OpenCode, or Kimi, plus an optional Codex home path, a shared OpenRouter API key, and a Kimi API key. Pi is embedded through CUT3's Node dependency instead of a separate binary override; CUT3 reads Pi auth/models config from `~/.pi/agent`, keeps Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes disabled so workspace instructions still come only from CUT3, and now surfaces authenticated Pi provider/model ids directly in the picker and `/model` suggestions instead of only showing a static `pi/default` placeholder. OpenCode account authentication still happens outside CUT3 through `opencode auth login` and `opencode auth logout`, while MCP server auth/debug remains server-specific through commands like `opencode mcp auth ` and `opencode mcp debug `. The OpenCode settings panel inspects the resolved OpenCode config paths plus `opencode auth list`, `opencode mcp list`, and `opencode mcp auth list` so CUT3 can show current credentials, provider-specific MCP status (including disabled and auth-gated entries), and copyable recovery commands. Kimi CLI authentication can use either `kimi login` or the in-shell `/login` flow when you are not using an API key, and new OpenCode sessions now inherit that shared OpenRouter key as `OPENROUTER_API_KEY` when the OpenCode provider config expects it. -- **OpenRouter free models**: review the current OpenRouter entries that are explicitly free-locked and compatible with CUT3's native tool-calling path (`tools` plus `tool_choice`), keep the built-in `openrouter/free` router handy, and pin any listed model into the picker. CUT3 now keeps a last-known-good OpenRouter free-model catalog locally so the picker and settings can stay usable even when the next live catalog refresh fails. +- **Provider overrides**: set custom binary paths for Codex, Copilot, OpenCode, or Kimi, plus an optional Codex home path, a shared OpenRouter API key, and a Kimi API key. Pi is embedded through Rowl's Node dependency instead of a separate binary override; Rowl reads Pi auth/models config from `~/.pi/agent`, keeps Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes disabled so workspace instructions still come only from Rowl, and now surfaces authenticated Pi provider/model ids directly in the picker and `/model` suggestions instead of only showing a static `pi/default` placeholder. OpenCode account authentication still happens outside Rowl through `opencode auth login` and `opencode auth logout`, while MCP server auth/debug remains server-specific through commands like `opencode mcp auth ` and `opencode mcp debug `. The OpenCode settings panel inspects the resolved OpenCode config paths plus `opencode auth list`, `opencode mcp list`, and `opencode mcp auth list` so Rowl can show current credentials, provider-specific MCP status (including disabled and auth-gated entries), and copyable recovery commands. Kimi CLI authentication can use either `kimi login` or the in-shell `/login` flow when you are not using an API key, and new OpenCode sessions now inherit that shared OpenRouter key as `OPENROUTER_API_KEY` when the OpenCode provider config expects it. +- **OpenRouter free models**: review the current OpenRouter entries that are explicitly free-locked and compatible with Rowl's native tool-calling path (`tools` plus `tool_choice`), keep the built-in `openrouter/free` router handy, and pin any listed model into the picker. Rowl now keeps a last-known-good OpenRouter free-model catalog locally so the picker and settings can stay usable even when the next live catalog refresh fails. - **Custom model slugs**: save extra model ids for GitHub Copilot, OpenCode, Kimi, Pi provider/model ids such as `github-copilot/claude-sonnet-4.5`, custom Codex models, or current OpenRouter `:free` slugs so they appear in the model picker and `/model` suggestions. - **Picker controls**: the chat composer now uses a searchable grouped model picker with direct `Usage`, `Provider readiness`, and `Manage models` actions. -- **Favorites, recents, and visibility**: pin favorite models so they stay at the top of the picker, let CUT3 surface recent model choices ahead of the long tail, and hide or restore discovered/saved models without deleting them. Hidden models are removed from both the picker and `/model` suggestions until you show them again. +- **Favorites, recents, and visibility**: pin favorite models so they stay at the top of the picker, let Rowl surface recent model choices ahead of the long tail, and hide or restore discovered/saved models without deleting them. Hidden models are removed from both the picker and `/model` suggestions until you show them again. - **Thread defaults**: choose whether new draft threads start in `Local` or `New worktree`, and set thread sharing to `Manual`, `Auto` (create a share link after a new server-backed thread settles), or `Disabled` for new links. - **Codex service tier**: choose `Automatic`, `Fast`, or `Flex` as the default service tier for new Codex turns. -- **Per-turn controls**: the composer exposes provider-aware reasoning controls where CUT3 has a provider-specific contract today. Codex and GitHub Copilot expose provider-specific reasoning levels, Codex also supports a per-turn `Fast Mode` toggle, and Pi now surfaces its live model reasoning capability plus Pi thinking levels (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) for reasoning-capable Pi models while still preserving Pi defaults until you choose an override. +- **Per-turn controls**: the composer exposes provider-aware reasoning controls where Rowl has a provider-specific contract today. Codex and GitHub Copilot expose provider-specific reasoning levels, Codex also supports a per-turn `Fast Mode` toggle, and Pi now surfaces its live model reasoning capability plus Pi thinking levels (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) for reasoning-capable Pi models while still preserving Pi defaults until you choose an override. - **Usage dashboard**: click the composer context ring or the picker `Usage` action to open a unified usage dashboard for the current selection, including documented/live context limits, token breakdowns from the latest matching runtime snapshot, latest reported spend when the provider exposes it, and GitHub Copilot quota details. - **Response visibility**: choose whether assistant messages stream token-by-token and whether tool/work-log entries stay visible in the main timeline. - **Permission policies**: save persistent app-wide or project-scoped approval rules with `allow`, `ask`, or `deny` actions, request-kind filters, request-type/detail matching, and Build/Plan/Review presets. -The chat model picker now shows OpenRouter as its own top-level section, with the built-in `openrouter/free` router plus the current OpenRouter `:free` models that CUT3 can safely use for native tool-calling turns. The picker is searchable, grouped by provider, and can open in-chat provider setup and model-management surfaces without sending you into Settings first. +The chat model picker now shows OpenRouter as its own top-level section, with the built-in `openrouter/free` router plus the current OpenRouter `:free` models that Rowl can safely use for native tool-calling turns. The picker is searchable, grouped by provider, and can open in-chat provider setup and model-management surfaces without sending you into Settings first. For the full details, see [.docs/provider-settings.md](.docs/provider-settings.md). @@ -130,11 +130,11 @@ The chat toolbar exposes two additional execution controls: - **Runtime mode**: choose `Full access` for direct execution or `Supervised` for in-app command/file approvals. - **Interaction mode**: switch between normal `Chat` turns and `Plan` turns for plan-first collaboration. -Runtime mode sets the default sandbox and approval posture for new sessions. Persistent permission policies from Settings can still auto-allow, ask, or deny specific requests on top of that default when a provider raises an approval. Pi is the main exception to CUT3's usual external-runtime sandbox story: `Supervised` still gates Pi tools through the same approval UX, but Pi itself is embedded through CUT3's Node SDK rather than a separate OS sandbox. +Runtime mode sets the default sandbox and approval posture for new sessions. Persistent permission policies from Settings can still auto-allow, ask, or deny specific requests on top of that default when a provider raises an approval. Pi is the main exception to Rowl's usual external-runtime sandbox story: `Supervised` still gates Pi tools through the same approval UX, but Pi itself is embedded through Rowl's Node SDK rather than a separate OS sandbox. -When a plan is active, CUT3 can keep it open in a sidebar and export it by copying, downloading markdown, or saving it into the workspace. For Pi, CUT3 drives that mode by sending explicit plan-first instructions and switching Pi onto a read-only tool set for the turn. +When a plan is active, Rowl can keep it open in a sidebar and export it by copying, downloading markdown, or saving it into the workspace. For Pi, Rowl drives that mode by sending explicit plan-first instructions and switching Pi onto a read-only tool set for the turn. -Threads also expose collaboration and history controls directly in the chat surface. Use the thread actions menu or the composer slash commands (`/share`, `/unshare`, `/compact`, `/undo`, `/redo`, `/export`, `/details`) to manage the current thread. Shared snapshots open in a dedicated read-only route that can import the snapshot into another local project. Use `Undo` and `Redo` in the thread header to move through recent restore snapshots, use `Fork thread here` on individual messages to branch from that point, and use the diff panel to fork from a completed checkpoint. The sidebar now supports project/thread search, pinning, active/all/archived filters, project recent/manual sort, and thread archiving, while each project shows the 10 most recent matching threads before `Show more` expands the rest. When a provider emits task lifecycle events, CUT3 shows a compact task panel above the timeline. +Threads also expose collaboration and history controls directly in the chat surface. Use the thread actions menu or the composer slash commands (`/share`, `/unshare`, `/compact`, `/undo`, `/redo`, `/export`, `/details`) to manage the current thread. Shared snapshots open in a dedicated read-only route that can import the snapshot into another local project. Use `Undo` and `Redo` in the thread header to move through recent restore snapshots, use `Fork thread here` on individual messages to branch from that point, and use the diff panel to fork from a completed checkpoint. The sidebar now supports project/thread search, pinning, active/all/archived filters, project recent/manual sort, and thread archiving, while each project shows the 10 most recent matching threads before `Show more` expands the rest. When a provider emits task lifecycle events, Rowl shows a compact task panel above the timeline. For the full details, see [.docs/runtime-modes.md](.docs/runtime-modes.md). @@ -142,7 +142,6 @@ For the full details, see [.docs/runtime-modes.md](.docs/runtime-modes.md). - [Codex prerequisites](.docs/codex-prerequisites.md) - [Desktop architecture and verification](apps/desktop/README.md) -- [GLM and MiniMax support plan](.docs/glm-minimax-support-plan.md) - [Quick start](.docs/quick-start.md) - [Runtime modes](.docs/runtime-modes.md) @@ -157,4 +156,4 @@ We are not accepting contributions yet. Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening an issue or PR. If you are using coding agents while contributing, also read [AGENTS.md](./AGENTS.md) for the current documentation hygiene and delegation rules. -Need support? Join the [Discord](https://discord.gg/jn4EGJjrvv). +Need support? Join the [Discord](https://discord.gg/jj). diff --git a/REMOTE.md b/REMOTE.md index 65227821077..deff2687e46 100644 --- a/REMOTE.md +++ b/REMOTE.md @@ -1,22 +1,22 @@ # Remote Access Setup -Use this when you want to open CUT3 from another device (phone, tablet, another laptop). +Use this when you want to open Rowl from another device (phone, tablet, another laptop). ## CLI ↔ Env option map -The CUT3 CLI accepts the following configuration options, available either as CLI flags or environment variables: +The Rowl CLI accepts the following configuration options, available either as CLI flags or environment variables: | CLI flag | Env var | Notes | | ----------------------------------- | -------------------------------------- | --------------------------------------------------------------------------- | -| `--mode ` | `CUT3_MODE` | Runtime mode. | -| `--port ` | `CUT3_PORT` | HTTP/WebSocket port. | -| `--host
` | `CUT3_HOST` | Bind interface/address. | -| `--state-dir ` | `CUT3_STATE_DIR` | State directory. | +| `--mode ` | `ROWL_MODE` | Runtime mode. | +| `--port ` | `ROWL_PORT` | HTTP/WebSocket port. | +| `--host
` | `ROWL_HOST` | Bind interface/address. | +| `--state-dir ` | `ROWL_STATE_DIR` | State directory. | | `--dev-url ` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. | -| `--no-browser` | `CUT3_NO_BROWSER` | Disable auto-open browser. | -| `--auth-token ` | `CUT3_AUTH_TOKEN` | WebSocket auth token. | -| `--auto-bootstrap-project-from-cwd` | `CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD` | Create a project for the current working directory on startup when missing. | -| `--log-websocket-events` | `CUT3_LOG_WS_EVENTS` | Emit server-side logs for outbound WebSocket push traffic. | +| `--no-browser` | `ROWL_NO_BROWSER` | Disable auto-open browser. | +| `--auth-token ` | `ROWL_AUTH_TOKEN` | WebSocket auth token. | +| `--auto-bootstrap-project-from-cwd` | `ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD` | Create a project for the current working directory on startup when missing. | +| `--log-websocket-events` | `ROWL_LOG_WS_EVENTS` | Emit server-side logs for outbound WebSocket push traffic. | > TIP: `--auth-token` also has a `--token` alias, and `--log-websocket-events` also has a `--log-ws-events` alias. > Use the `--help` flag to see all available options and their descriptions. diff --git a/CUT3.png b/Rowl.png similarity index 100% rename from CUT3.png rename to Rowl.png diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 48ce39f4c6e..dd5bfab12e2 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -1,6 +1,6 @@ # Desktop -CUT3 desktop wraps the web UI in Electron and starts a desktop-scoped `t3` backend on loopback. The renderer still talks to the same WebSocket and orchestration surface as the browser app, while the Electron main process owns native dialogs, desktop secrets, menus, backend lifecycle, and update UX. +Rowl desktop wraps the web UI in Electron and starts a desktop-scoped `t3` backend on loopback. The renderer still talks to the same WebSocket and orchestration surface as the browser app, while the Electron main process owns native dialogs, desktop secrets, menus, backend lifecycle, and update UX. ## Key files @@ -15,10 +15,10 @@ CUT3 desktop wraps the web UI in Electron and starts a desktop-scoped `t3` backe 1. `app.whenReady()` runs in `src/main.ts`. 2. Electron registers IPC handlers and generates a desktop backend auth token. -3. Electron spawns the bundled `apps/server/dist` entry with `CUT3_MODE=desktop`, `CUT3_PORT=0`, and `CUT3_AUTH_TOKEN`. -4. The backend emits a readiness line prefixed with `[cut3-desktop-ready]` once it has selected a loopback port. +3. Electron spawns the bundled `apps/server/dist` entry with `ROWL_MODE=desktop`, `ROWL_PORT=0`, and `ROWL_AUTH_TOKEN`. +4. The backend emits a readiness line prefixed with `[rowl-desktop-ready]` once it has selected a loopback port. 5. Electron converts that port into a renderer-safe WebSocket URL and broadcasts it through `desktopBridge.onBackendWsUrlUpdated`. -6. The main window loads either the Vite dev server or the packaged `cut3://app/index.html` document. +6. The main window loads either the Vite dev server or the packaged `rowl://app/index.html` document. ## Development and local launch @@ -28,8 +28,8 @@ CUT3 desktop wraps the web UI in Electron and starts a desktop-scoped `t3` backe ## State and logs -- Desktop state defaults to `CUT3_STATE_DIR` when it is set. -- Without an override, desktop state lives under `~/.t3/cut3`. +- Desktop state defaults to `ROWL_STATE_DIR` when it is set. +- Without an override, desktop state lives under `~/.t3/rowl`. - Packaged builds capture main-process logs in `/logs/desktop-main.log` and backend child logs in `/logs/server-child.log`. - Development launches forward main-process bootstrap headers plus backend stdout and stderr to the parent terminal instead of rotating packaged log files. @@ -38,13 +38,13 @@ CUT3 desktop wraps the web UI in Electron and starts a desktop-scoped `t3` backe - The release workflow builds `apps/desktop/dist-electron` plus `apps/server/dist` once in Linux preflight, archives that bundle, and reuses it in each packaging job via `bun run dist:desktop:artifact -- --skip-build`. - Release assets are published for all supported desktop targets from one tag: macOS arm64/x64, Linux x64, and Windows x64. - The workflow always publishes `SHA256SUMS` for the final release assets. -- macOS and Windows signing remain secret-driven, and releases can be configured to fail instead of silently shipping unsigned installers by enabling `CUT3_REQUIRE_SIGNING=true` (or the matching manual workflow input). +- macOS and Windows signing remain secret-driven, and releases can be configured to fail instead of silently shipping unsigned installers by enabling `ROWL_REQUIRE_SIGNING=true` (or the matching manual workflow input). - Manual release workflow runs also support `dry_run=true`, which validates packaging/signing/checksum steps and uploads the assembled assets as a workflow artifact without publishing a GitHub Release or updating `main`. ## Smoke test contract - Run `bun run test:desktop-smoke` from the repo root or `bun run smoke-test` inside `apps/desktop`. -- The smoke test launches `dist-electron/main.js`, waits for the `[cut3-desktop-ready]{...}` backend readiness marker, and fails on launch-time fatal errors. +- The smoke test launches `dist-electron/main.js`, waits for the `[rowl-desktop-ready]{...}` backend readiness marker, and fails on launch-time fatal errors. - The smoke test does not validate full renderer interaction, menu behavior, or update installation. ## Related docs diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 51fddf46b8c..93efeadc34d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -27,5 +27,5 @@ "vitest": "catalog:", "wait-on": "^8.0.2" }, - "productName": "CUT3" + "productName": "Rowl" } diff --git a/apps/desktop/resources/icon.png b/apps/desktop/resources/icon.png index 7deef34a342..762d14ae76c 100644 Binary files a/apps/desktop/resources/icon.png and b/apps/desktop/resources/icon.png differ diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index 0e6a6188b4a..3bfb62d1760 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -51,7 +51,7 @@ function cleanupStaleDevApps() { return; } - spawnSync("pkill", ["-f", "--", `--cut3-dev-root=${desktopDir}`], { stdio: "ignore" }); + spawnSync("pkill", ["-f", "--", `--rowl-dev-root=${desktopDir}`], { stdio: "ignore" }); } function startApp() { @@ -63,13 +63,13 @@ function startApp() { const linuxDesktopLaunchEnv = resolveLinuxDesktopLaunchEnv({ electronBinaryPath: electronPath, mainEntryPath: "dist-electron/main.js", - extraArgs: [`--cut3-dev-root=${desktopDir}`], + extraArgs: [`--rowl-dev-root=${desktopDir}`], extraEnv: { VITE_DEV_SERVER_URL: devServerUrl, }, }); - const app = spawn(electronPath, [`--cut3-dev-root=${desktopDir}`, "dist-electron/main.js"], { + const app = spawn(electronPath, [`--rowl-dev-root=${desktopDir}`, "dist-electron/main.js"], { cwd: desktopDir, env: { ...childEnv, diff --git a/apps/desktop/scripts/smoke-test-lib.mjs b/apps/desktop/scripts/smoke-test-lib.mjs index c38ea42f104..6afa4b51515 100644 --- a/apps/desktop/scripts/smoke-test-lib.mjs +++ b/apps/desktop/scripts/smoke-test-lib.mjs @@ -1,4 +1,4 @@ -export const DESKTOP_BACKEND_READY_PREFIX = "[cut3-desktop-ready]"; +export const DESKTOP_BACKEND_READY_PREFIX = "[rowl-desktop-ready]"; export const DESKTOP_BOOTSTRAP_READY_PATTERN = /bootstrap backend ready port=(\d+)/; export const READY_TIMEOUT_MS = 15_000; export const READY_SETTLE_MS = 1_500; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d37337c3d46..36c034ea068 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -70,7 +70,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state"; const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download"; const UPDATE_INSTALL_CHANNEL = "desktop:update-install"; -const DESKTOP_SCHEME = "cut3"; +const DESKTOP_SCHEME = "rowl"; const ROOT_DIR = Path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const appReleaseBranding = resolveAppReleaseBranding({ @@ -78,7 +78,7 @@ const appReleaseBranding = resolveAppReleaseBranding({ isDevelopment, }); const STATE_DIR = - process.env.CUT3_STATE_DIR?.trim() || + process.env.ROWL_STATE_DIR?.trim() || Path.join(OS.homedir(), ".t3", appReleaseBranding.stateDirName); const APP_DISPLAY_NAME = appReleaseBranding.displayName; const APP_USER_MODEL_ID = appReleaseBranding.appId; @@ -530,8 +530,8 @@ function resolveEmbeddedCommitHash(): string | null { try { const raw = FS.readFileSync(packageJsonPath, "utf8"); - const parsed = JSON.parse(raw) as { cut3CommitHash?: unknown }; - return normalizeCommitHash(parsed.cut3CommitHash); + const parsed = JSON.parse(raw) as { rowlCommitHash?: unknown }; + return normalizeCommitHash(parsed.rowlCommitHash); } catch { return null; } @@ -542,7 +542,7 @@ function resolveAboutCommitHash(): string | null { return aboutCommitHashCache; } - const envCommitHash = normalizeCommitHash(process.env.CUT3_COMMIT_HASH); + const envCommitHash = normalizeCommitHash(process.env.ROWL_COMMIT_HASH); if (envCommitHash) { aboutCommitHashCache = envCommitHash; return aboutCommitHashCache; @@ -564,7 +564,7 @@ function resolveBackendEntry(): string { } function resolveBackendCwd(): string { - const override = process.env.CUT3_BACKEND_CWD?.trim(); + const override = process.env.ROWL_BACKEND_CWD?.trim(); if (override) { return override; } @@ -630,7 +630,7 @@ function handleFatalStartupError(stage: string, error: unknown): void { console.error(`[desktop] fatal startup error (${stage})`, error); if (!isQuitting) { isQuitting = true; - dialog.showErrorBox("CUT3 failed to start", `Stage: ${stage}\n${message}${detail}`); + dialog.showErrorBox("Rowl failed to start", `Stage: ${stage}\n${message}${detail}`); } stopBackend(); restoreStdIoCapture?.(); @@ -651,7 +651,7 @@ function updateBackendWsUrl(port: number): void { port, authToken: backendAuthToken, }); - process.env.CUT3_DESKTOP_WS_URL = backendWsUrl; + process.env.ROWL_DESKTOP_WS_URL = backendWsUrl; writeDesktopLogHeader(`backend websocket url updated port=${port}`); broadcastBackendWsUrl(); } @@ -747,7 +747,7 @@ function handleCheckForUpdatesMenuClick(): void { isPackaged: app.isPackaged, platform: process.platform, appImage: process.env.APPIMAGE, - disabledByEnv: process.env.CUT3_DISABLE_AUTO_UPDATE === "1", + disabledByEnv: process.env.ROWL_DISABLE_AUTO_UPDATE === "1", }); if (disabledReason) { console.info("[desktop-updater] Manual update check requested, but updates are disabled."); @@ -774,7 +774,7 @@ async function checkForUpdatesFromMenu(): Promise { void dialog.showMessageBox({ type: "info", title: "You're up to date!", - message: `CUT3 ${updateState.currentVersion} is currently the newest version available.`, + message: `Rowl ${updateState.currentVersion} is currently the newest version available.`, buttons: ["OK"], }); } else if (updateState.status === "error") { @@ -928,7 +928,7 @@ function resolveWindowIcon(): Electron.NativeImage | null { * parentheses (e.g. `~/.config/T3 Code (Alpha)` on Linux). This is * unfriendly for shell usage and violates Linux naming conventions. * - * We override it to a clean lowercase name (`cut3`). If the legacy + * We override it to a clean lowercase name (`rowl`). If the legacy * directory already exists we keep using it so existing users do not * lose their Chromium profile data (localStorage, cookies, sessions). */ @@ -999,7 +999,7 @@ function shouldEnableAutoUpdates(): boolean { isPackaged: app.isPackaged, platform: process.platform, appImage: process.env.APPIMAGE, - disabledByEnv: process.env.CUT3_DISABLE_AUTO_UPDATE === "1", + disabledByEnv: process.env.ROWL_DISABLE_AUTO_UPDATE === "1", }) === null ); } @@ -1084,7 +1084,7 @@ function configureAutoUpdater(): void { updaterConfigured = true; const githubToken = - process.env.CUT3_DESKTOP_UPDATE_GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || ""; + process.env.ROWL_DESKTOP_UPDATE_GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || ""; if (githubToken) { // When a token is provided, re-configure the feed with `private: true` so // electron-updater uses the GitHub API (api.github.com) instead of the @@ -1187,11 +1187,11 @@ function configureAutoUpdater(): void { function backendEnv(): NodeJS.ProcessEnv { return { ...process.env, - CUT3_MODE: "desktop", - CUT3_NO_BROWSER: "1", - CUT3_PORT: "0", - CUT3_STATE_DIR: STATE_DIR, - CUT3_AUTH_TOKEN: backendAuthToken, + ROWL_MODE: "desktop", + ROWL_NO_BROWSER: "1", + ROWL_PORT: "0", + ROWL_STATE_DIR: STATE_DIR, + ROWL_AUTH_TOKEN: backendAuthToken, }; } @@ -1598,7 +1598,7 @@ function createWindow(): BrowserWindow { contextIsolation: true, nodeIntegration: false, sandbox: true, - additionalArguments: backendWsUrl ? [`--cut3-desktop-ws-url=${backendWsUrl}`] : [], + additionalArguments: backendWsUrl ? [`--rowl-desktop-ws-url=${backendWsUrl}`] : [], }, }); diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index ee5790f56e9..176760375b6 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -15,7 +15,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state"; const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download"; const UPDATE_INSTALL_CHANNEL = "desktop:update-install"; -let wsUrl = process.env.CUT3_DESKTOP_WS_URL ?? null; +let wsUrl = process.env.ROWL_DESKTOP_WS_URL ?? null; wsUrl = resolveInitialDesktopWsUrl({ envValue: wsUrl, argv: process.argv }); ipcRenderer.on(BACKEND_WS_URL_UPDATED_CHANNEL, (_event, nextUrl: unknown) => { diff --git a/apps/desktop/src/preloadWsUrl.test.ts b/apps/desktop/src/preloadWsUrl.test.ts index 940f357f6b6..aec43335fe5 100644 --- a/apps/desktop/src/preloadWsUrl.test.ts +++ b/apps/desktop/src/preloadWsUrl.test.ts @@ -7,7 +7,7 @@ describe("resolveInitialDesktopWsUrl", () => { expect( resolveInitialDesktopWsUrl({ envValue: "ws://127.0.0.1:3773/?token=env-token", - argv: ["electron", "app.js", "--cut3-desktop-ws-url=ws://127.0.0.1:4000"], + argv: ["electron", "app.js", "--rowl-desktop-ws-url=ws://127.0.0.1:4000"], }), ).toBe("ws://127.0.0.1:3773/?token=env-token"); }); @@ -16,7 +16,7 @@ describe("resolveInitialDesktopWsUrl", () => { expect( resolveInitialDesktopWsUrl({ envValue: undefined, - argv: ["electron", "app.js", "--cut3-desktop-ws-url=ws://127.0.0.1:4000/?token=test"], + argv: ["electron", "app.js", "--rowl-desktop-ws-url=ws://127.0.0.1:4000/?token=test"], }), ).toBe("ws://127.0.0.1:4000/?token=test"); }); diff --git a/apps/desktop/src/preloadWsUrl.ts b/apps/desktop/src/preloadWsUrl.ts index 66e96965025..7bddd4a31eb 100644 --- a/apps/desktop/src/preloadWsUrl.ts +++ b/apps/desktop/src/preloadWsUrl.ts @@ -1,4 +1,4 @@ -const DESKTOP_WS_URL_ARG_PREFIX = "--cut3-desktop-ws-url="; +const DESKTOP_WS_URL_ARG_PREFIX = "--rowl-desktop-ws-url="; export function resolveInitialDesktopWsUrl(args: { envValue: string | null | undefined; diff --git a/apps/desktop/src/updateState.test.ts b/apps/desktop/src/updateState.test.ts index 8ab628f6cac..7017570a139 100644 --- a/apps/desktop/src/updateState.test.ts +++ b/apps/desktop/src/updateState.test.ts @@ -86,7 +86,7 @@ describe("getAutoUpdateDisabledReason", () => { appImage: undefined, disabledByEnv: true, }), - ).toContain("CUT3_DISABLE_AUTO_UPDATE"); + ).toContain("ROWL_DISABLE_AUTO_UPDATE"); }); it("reports linux non-AppImage builds as disabled", () => { diff --git a/apps/desktop/src/updateState.ts b/apps/desktop/src/updateState.ts index c1ecbe89081..c80508ba3d0 100644 --- a/apps/desktop/src/updateState.ts +++ b/apps/desktop/src/updateState.ts @@ -42,7 +42,7 @@ export function getAutoUpdateDisabledReason(args: { return "Automatic updates are only available in packaged production builds."; } if (args.disabledByEnv) { - return "Automatic updates are disabled by the CUT3_DISABLE_AUTO_UPDATE setting."; + return "Automatic updates are disabled by the ROWL_DISABLE_AUTO_UPDATE setting."; } if (args.platform === "linux" && !args.appImage) { return "Automatic updates on Linux require running the AppImage build."; diff --git a/apps/desktop/src/userDataPath.test.ts b/apps/desktop/src/userDataPath.test.ts index dd157d92c21..8f3fe2544f5 100644 --- a/apps/desktop/src/userDataPath.test.ts +++ b/apps/desktop/src/userDataPath.test.ts @@ -3,8 +3,9 @@ import { describe, expect, it } from "vitest"; import { getLegacyUserDataDirNames, resolveDesktopUserDataPath } from "./userDataPath"; describe("getLegacyUserDataDirNames", () => { - it("keeps the unified CUT3 profile compatible with legacy T3 directories", () => { - expect(getLegacyUserDataDirNames({ appDisplayName: "CUT3" })).toEqual([ + it("keeps the unified Rowl profile compatible with legacy directories", () => { + expect(getLegacyUserDataDirNames({ appDisplayName: "Rowl" })).toEqual([ + "Rowl", "CUT3", "T3 Code", "T3 Code (Alpha)", @@ -20,8 +21,8 @@ describe("resolveDesktopUserDataPath", () => { expect( resolveDesktopUserDataPath({ appDataBase: "/config", - userDataDirName: "cut3", - legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "CUT3" }), + userDataDirName: "rowl", + legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "Rowl" }), pathExists: (path) => existingPaths.has(path), }), ).toBe("/config/CUT3"); @@ -31,11 +32,11 @@ describe("resolveDesktopUserDataPath", () => { expect( resolveDesktopUserDataPath({ appDataBase: "/config", - userDataDirName: "cut3", - legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "CUT3" }), + userDataDirName: "rowl", + legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "Rowl" }), pathExists: () => false, }), - ).toBe("/config/cut3"); + ).toBe("/config/rowl"); }); it("can recover the old alpha directory too", () => { @@ -44,8 +45,8 @@ describe("resolveDesktopUserDataPath", () => { expect( resolveDesktopUserDataPath({ appDataBase: "/config", - userDataDirName: "cut3", - legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "CUT3" }), + userDataDirName: "rowl", + legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "Rowl" }), pathExists: (path) => existingPaths.has(path), }), ).toBe("/config/T3 Code (Alpha)"); @@ -57,8 +58,8 @@ describe("resolveDesktopUserDataPath", () => { expect( resolveDesktopUserDataPath({ appDataBase: "/config", - userDataDirName: "cut3", - legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "CUT3" }), + userDataDirName: "rowl", + legacyDirNames: getLegacyUserDataDirNames({ appDisplayName: "Rowl" }), pathExists: (path) => existingPaths.has(path), }), ).toBe("/config/T3 Code (Dev)"); diff --git a/apps/desktop/src/userDataPath.ts b/apps/desktop/src/userDataPath.ts index 6cacae9866d..e9ff3191512 100644 --- a/apps/desktop/src/userDataPath.ts +++ b/apps/desktop/src/userDataPath.ts @@ -1,6 +1,6 @@ import Path from "node:path"; -const LEGACY_USER_DATA_DIR_NAMES = ["T3 Code", "T3 Code (Alpha)", "T3 Code (Dev)"] as const; +const LEGACY_USER_DATA_DIR_NAMES = ["CUT3", "T3 Code", "T3 Code (Alpha)", "T3 Code (Dev)"] as const; function joinUserDataPath(basePath: string, segment: string): string { const pathModule = basePath.includes("/") ? Path.posix : Path.win32; diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc index 7ab7be48792..e16a6d93f9d 100644 --- a/apps/desktop/turbo.jsonc +++ b/apps/desktop/turbo.jsonc @@ -7,16 +7,16 @@ "outputs": ["dist-electron/**"], }, "dev": { - "dependsOn": ["cut3#build"], + "dependsOn": ["rowl#build"], "persistent": true, }, "start": { - "dependsOn": ["build", "@t3tools/web#build", "cut3#build"], + "dependsOn": ["build", "@t3tools/web#build", "rowl#build"], "cache": false, "persistent": true, }, "smoke-test": { - "dependsOn": ["build", "@t3tools/web#build", "cut3#build"], + "dependsOn": ["build", "@t3tools/web#build", "rowl#build"], "cache": false, "outputs": [], }, diff --git a/apps/marketing/src/lib/releases.ts b/apps/marketing/src/lib/releases.ts index f2a144ff212..dc31d906ea4 100644 --- a/apps/marketing/src/lib/releases.ts +++ b/apps/marketing/src/lib/releases.ts @@ -3,7 +3,7 @@ const REPO = "yappologistic/t3code"; export const RELEASES_URL = `https://github.com/${REPO}/releases`; const API_URL = `https://api.github.com/repos/${REPO}/releases/latest`; -const CACHE_KEY = "cut3-latest-release"; +const CACHE_KEY = "rowl-latest-release"; export interface ReleaseAsset { name: string; diff --git a/apps/server/package.json b/apps/server/package.json index 11059746c69..8abc771c3c0 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,5 +1,5 @@ { - "name": "cut3", + "name": "rowl", "version": "1.0.1", "repository": { "type": "git", @@ -7,7 +7,7 @@ "directory": "apps/server" }, "bin": { - "cut3": "./dist/index.mjs" + "rowl": "./dist/index.mjs" }, "files": [ "dist" diff --git a/apps/server/src/attachmentStore.test.ts b/apps/server/src/attachmentStore.test.ts index 67e5868eecd..5f34a5a412d 100644 --- a/apps/server/src/attachmentStore.test.ts +++ b/apps/server/src/attachmentStore.test.ts @@ -44,7 +44,7 @@ describe("attachmentStore", () => { }); it("resolves attachment path by id using the extension that exists on disk", () => { - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-attachment-store-")); + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-attachment-store-")); try { const attachmentId = "thread-1-attachment"; const attachmentsDir = path.join(stateDir, "attachments"); @@ -63,7 +63,7 @@ describe("attachmentStore", () => { }); it("returns null when no attachment file exists for the id", () => { - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-attachment-store-")); + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-attachment-store-")); try { const resolved = resolveAttachmentPathById({ stateDir, diff --git a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts index 2f79ea9d5a5..70e79672c14 100644 --- a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts @@ -43,6 +43,7 @@ function makeSnapshot(input: { id: input.threadId, projectId: input.projectId, title: "Thread", + goal: null, model: "gpt-5-codex", interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, runtimeMode: "full-access", diff --git a/apps/server/src/checkpointing/Layers/CheckpointStore.ts b/apps/server/src/checkpointing/Layers/CheckpointStore.ts index 2e9a7a18006..c5fe707f970 100644 --- a/apps/server/src/checkpointing/Layers/CheckpointStore.ts +++ b/apps/server/src/checkpointing/Layers/CheckpointStore.ts @@ -99,10 +99,10 @@ const makeCheckpointStore = Effect.gen(function* () { const commitEnv: NodeJS.ProcessEnv = { ...process.env, GIT_INDEX_FILE: tempIndexPath, - GIT_AUTHOR_NAME: "CUT3", - GIT_AUTHOR_EMAIL: "cut3@users.noreply.github.com", - GIT_COMMITTER_NAME: "CUT3", - GIT_COMMITTER_EMAIL: "cut3@users.noreply.github.com", + GIT_AUTHOR_NAME: "Rowl", + GIT_AUTHOR_EMAIL: "rowl@users.noreply.github.com", + GIT_COMMITTER_NAME: "Rowl", + GIT_COMMITTER_EMAIL: "rowl@users.noreply.github.com", }; const headExists = yield* hasHeadCommit(input.cwd); diff --git a/apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts b/apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts index 7d54a565174..26600e45bd3 100644 --- a/apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts +++ b/apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts @@ -46,4 +46,4 @@ export interface CheckpointDiffQueryShape { export class CheckpointDiffQuery extends ServiceMap.Service< CheckpointDiffQuery, CheckpointDiffQueryShape ->()("cut3/checkpointing/Services/CheckpointDiffQuery") {} +>()("rowl/checkpointing/Services/CheckpointDiffQuery") {} diff --git a/apps/server/src/checkpointing/Services/CheckpointStore.ts b/apps/server/src/checkpointing/Services/CheckpointStore.ts index c4995a75c8d..d7182c63b4b 100644 --- a/apps/server/src/checkpointing/Services/CheckpointStore.ts +++ b/apps/server/src/checkpointing/Services/CheckpointStore.ts @@ -96,5 +96,5 @@ export interface CheckpointStoreShape { * CheckpointStore - Service tag for checkpoint persistence and restore operations. */ export class CheckpointStore extends ServiceMap.Service()( - "cut3/checkpointing/Services/CheckpointStore", + "rowl/checkpointing/Services/CheckpointStore", ) {} diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index 3970d3a6f14..d4e3729bda1 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -905,8 +905,8 @@ describe("startSession", () => { it("enables Codex experimental api capabilities during initialize", () => { expect(buildCodexInitializeParams()).toEqual({ clientInfo: { - name: "cut3_desktop", - title: "CUT3 Desktop", + name: "rowl_desktop", + title: "Rowl Desktop", version: "0.1.0", }, capabilities: { @@ -973,7 +973,7 @@ describe("startSession", () => { ) .mockImplementation(() => { throw new Error( - "Codex CLI v0.36.0 is too old for CUT3. Upgrade to v0.37.0 or newer and restart CUT3.", + "Codex CLI v0.36.0 is too old for Rowl. Upgrade to v0.37.0 or newer and restart Rowl.", ); }); @@ -985,7 +985,7 @@ describe("startSession", () => { runtimeMode: "full-access", }), ).rejects.toThrow( - "Codex CLI v0.36.0 is too old for CUT3. Upgrade to v0.37.0 or newer and restart CUT3.", + "Codex CLI v0.36.0 is too old for Rowl. Upgrade to v0.37.0 or newer and restart Rowl.", ); expect(versionCheck).toHaveBeenCalledTimes(1); expect(events).toEqual([ @@ -993,7 +993,7 @@ describe("startSession", () => { method: "session/startFailed", kind: "error", message: - "Codex CLI v0.36.0 is too old for CUT3. Upgrade to v0.37.0 or newer and restart CUT3.", + "Codex CLI v0.36.0 is too old for Rowl. Upgrade to v0.37.0 or newer and restart Rowl.", }, ]); } finally { @@ -1013,7 +1013,7 @@ describe("startSession", () => { }); }); - const tempDir = mkdtempSync(path.join(os.tmpdir(), "cut3-codex-failfast-")); + const tempDir = mkdtempSync(path.join(os.tmpdir(), "rowl-codex-failfast-")); const fakeBinaryPath = path.join( tempDir, process.platform === "win32" ? "fake-codex.cmd" : "fake-codex", diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index 910c94392f8..33c6d57e387 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -454,8 +454,8 @@ export function normalizeCodexModelSlug( export function buildCodexInitializeParams() { return { clientInfo: { - name: "cut3_desktop", - title: "CUT3 Desktop", + name: "rowl_desktop", + title: "Rowl Desktop", version: "0.1.0", }, capabilities: { @@ -1084,7 +1084,7 @@ export class CodexAppServerManager extends EventEmitter()( - "cut3/config/ServerConfig", + "rowl/config/ServerConfig", ) { static readonly layerTest = (cwd: string, statedir: string) => Layer.effect( diff --git a/apps/server/src/copilotAcpManager.ts b/apps/server/src/copilotAcpManager.ts index 464bb3a5065..c6fc22abfdb 100644 --- a/apps/server/src/copilotAcpManager.ts +++ b/apps/server/src/copilotAcpManager.ts @@ -29,6 +29,7 @@ import { mapToolKindToItemType, mapToolKindToRequestType, permissionDecisionFromOutcome, + providerLog, readResumeSessionId, summarizeToolContent, toMessage, @@ -774,6 +775,11 @@ export class CopilotAcpManager extends EventEmitter { payload: context.session.model ? { model: context.session.model } : {}, }); + providerLog( + "Turn", + `Starting turn ${turnId} for thread ${input.threadId}, model: ${context.session.model ?? "default"}`, + ); + try { const result = await context.connection.prompt({ sessionId: context.acpSessionId, @@ -785,6 +791,8 @@ export class CopilotAcpManager extends EventEmitter { ], }); + providerLog("Turn", `Turn ${turnId} completed with stopReason: ${result.stopReason}`); + this.emitRuntimeEvent({ ...this.createEventBase(context), turnId, @@ -855,7 +863,7 @@ export class CopilotAcpManager extends EventEmitter { } async respondToUserInput(): Promise { - throw new Error("GitHub Copilot CLI does not expose structured user input requests in CUT3."); + throw new Error("GitHub Copilot CLI does not expose structured user input requests in Rowl."); } async stopSession(threadId: ThreadId): Promise { @@ -889,6 +897,7 @@ export class CopilotAcpManager extends EventEmitter { } private async disposeContext(context: CopilotSessionContext): Promise { + providerLog("Session", `Disposing session for thread ${context.session.threadId}`); context.stopping = true; this.resolvePendingApprovalsAsCancelled(context); try { @@ -897,12 +906,18 @@ export class CopilotAcpManager extends EventEmitter { // Best-effort cancellation only. } - killChildTree(context.child); + await new Promise((resolve) => setTimeout(resolve, 500)); + + if (!context.child.killed) { + providerLog("Session", `Sending SIGKILL to child process ${context.child.pid}`); + killChildTree(context.child, "SIGKILL"); + } this.updateSession(context, { status: "closed", activeTurnId: undefined, }); this.deleteTrackedSession(context.session.threadId, context); + providerLog("Session", `Session disposed for thread ${context.session.threadId}`); } async listSessions(): Promise> { diff --git a/apps/server/src/copilotUsage.ts b/apps/server/src/copilotUsage.ts index 23300734cb9..aba0d3dee4a 100644 --- a/apps/server/src/copilotUsage.ts +++ b/apps/server/src/copilotUsage.ts @@ -217,7 +217,7 @@ async function fetchCopilotInternalUser(params: { headers: { Accept: "application/json", Authorization: `token ${params.token}`, - "User-Agent": "cut3", + "User-Agent": "rowl", "X-GitHub-Api-Version": COPILOT_USAGE_API_VERSION, }, signal, diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts index 14546813115..d7712baa13d 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts @@ -115,7 +115,7 @@ function withFakeCodexEnv( return Effect.acquireUseRelease( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "cut3-codex-text-" }); + const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "rowl-codex-text-" }); const binDir = yield* makeFakeCodexBinary(tempDir); const previousPath = process.env.PATH; const previousOutput = process.env.T3_FAKE_CODEX_OUTPUT_B64; diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index b353a00bb92..5b34b7033de 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -131,7 +131,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { prefix: string, content: string, ): Effect.Effect => { - const filePath = path.join(tempDir, `cut3-${prefix}-${process.pid}-${randomUUID()}.tmp`); + const filePath = path.join(tempDir, `rowl-${prefix}-${process.pid}-${randomUUID()}.tmp`); return fileSystem.writeFileString(filePath, content).pipe( Effect.mapError( (cause) => diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index a86b9891e38..8b5fb6c96c0 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -889,24 +889,24 @@ it.layer(TestLayer)("git integration", (it) => { Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); - yield* createGitBranch({ cwd: tmp, branch: "cut3/feat/session" }); - yield* createGitBranch({ cwd: tmp, branch: "cut3/tmp-working" }); - yield* checkoutGitBranch({ cwd: tmp, branch: "cut3/tmp-working" }); + yield* createGitBranch({ cwd: tmp, branch: "rowl/feat/session" }); + yield* createGitBranch({ cwd: tmp, branch: "rowl/tmp-working" }); + yield* checkoutGitBranch({ cwd: tmp, branch: "rowl/tmp-working" }); const renamed = yield* renameGitBranch({ cwd: tmp, - oldBranch: "cut3/tmp-working", - newBranch: "cut3/feat/session", + oldBranch: "rowl/tmp-working", + newBranch: "rowl/feat/session", }); - expect(renamed.branch).toBe("cut3/feat/session-1"); + expect(renamed.branch).toBe("rowl/feat/session-1"); const branches = yield* listGitBranches({ cwd: tmp }); - expect(branches.branches.some((branch) => branch.name === "cut3/feat/session")).toBe(true); - expect(branches.branches.some((branch) => branch.name === "cut3/feat/session-1")).toBe( + expect(branches.branches.some((branch) => branch.name === "rowl/feat/session")).toBe(true); + expect(branches.branches.some((branch) => branch.name === "rowl/feat/session-1")).toBe( true, ); const current = branches.branches.find((branch) => branch.current); - expect(current?.name).toBe("cut3/feat/session-1"); + expect(current?.name).toBe("rowl/feat/session-1"); }), ); @@ -914,18 +914,18 @@ it.layer(TestLayer)("git integration", (it) => { Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); - yield* createGitBranch({ cwd: tmp, branch: "cut3/feat/session" }); - yield* createGitBranch({ cwd: tmp, branch: "cut3/feat/session-1" }); - yield* createGitBranch({ cwd: tmp, branch: "cut3/tmp-working" }); - yield* checkoutGitBranch({ cwd: tmp, branch: "cut3/tmp-working" }); + yield* createGitBranch({ cwd: tmp, branch: "rowl/feat/session" }); + yield* createGitBranch({ cwd: tmp, branch: "rowl/feat/session-1" }); + yield* createGitBranch({ cwd: tmp, branch: "rowl/tmp-working" }); + yield* checkoutGitBranch({ cwd: tmp, branch: "rowl/tmp-working" }); const renamed = yield* renameGitBranch({ cwd: tmp, - oldBranch: "cut3/tmp-working", - newBranch: "cut3/feat/session", + oldBranch: "rowl/tmp-working", + newBranch: "rowl/feat/session", }); - expect(renamed.branch).toBe("cut3/feat/session-2"); + expect(renamed.branch).toBe("rowl/feat/session-2"); }), ); @@ -1326,12 +1326,12 @@ it.layer(TestLayer)("git integration", (it) => { yield* initRepoWithCommit(tmp); const core = yield* GitCore; - yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/cut3.git"]); + yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/rowl.git"]); const remoteName = yield* core.ensureRemote({ cwd: tmp, preferredName: "origin", - url: "git@github.com:pingdotgg/cut3.git/", + url: "git@github.com:pingdotgg/rowl.git/", }); expect(remoteName).toBe("origin"); @@ -1604,7 +1604,7 @@ it.layer(TestLayer)("git integration", (it) => { yield* git(tmp, [ "checkout", "-b", - "cut3/pr-488/statemachine", + "rowl/pr-488/statemachine", "--track", "jasonLaster/statemachine", ]); @@ -1626,7 +1626,7 @@ it.layer(TestLayer)("git integration", (it) => { yield* git(tmp, ["ls-remote", "--heads", "jasonLaster", "statemachine"]), ).toContain("statemachine"); expect( - yield* git(tmp, ["ls-remote", "--heads", "jasonLaster", "cut3/pr-488/statemachine"]), + yield* git(tmp, ["ls-remote", "--heads", "jasonLaster", "rowl/pr-488/statemachine"]), ).toBe(""); }), ); diff --git a/apps/server/src/git/Layers/GitManager.test.ts b/apps/server/src/git/Layers/GitManager.test.ts index db385cf46cf..936fce2e66b 100644 --- a/apps/server/src/git/Layers/GitManager.test.ts +++ b/apps/server/src/git/Layers/GitManager.test.ts @@ -143,7 +143,7 @@ function createBareRemote(): Effect.Effect< FileSystem.FileSystem | Scope.Scope | GitService > { return Effect.gen(function* () { - const remoteDir = yield* makeTempDir("cut3-git-remote-"); + const remoteDir = yield* makeTempDir("rowl-git-remote-"); yield* runGit(remoteDir, ["init", "--bare"]); return remoteDir; }); @@ -498,7 +498,7 @@ const GitManagerTestLayer = Layer.provideMerge(GitServiceLive, NodeServices.laye it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("status includes PR metadata when branch already has an open PR", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/status-open-pr"]); const remoteDir = yield* createBareRemote(); @@ -538,7 +538,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "status detects cross-repo PRs from the upstream remote URL owner", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const forkDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "fork-seed", forkDir]); @@ -547,7 +547,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { yield* runGit(repoDir, ["add", "fork-pr.txt"]); yield* runGit(repoDir, ["commit", "-m", "Fork PR branch"]); yield* runGit(repoDir, ["push", "-u", "fork-seed", "statemachine"]); - yield* runGit(repoDir, ["checkout", "-b", "cut3/pr-488/statemachine"]); + yield* runGit(repoDir, ["checkout", "-b", "rowl/pr-488/statemachine"]); yield* runGit(repoDir, ["branch", "--set-upstream-to", "fork-seed/statemachine"]); yield* runGit(repoDir, [ "config", @@ -576,7 +576,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }); const status = yield* manager.status({ cwd: repoDir }); - expect(status.branch).toBe("cut3/pr-488/statemachine"); + expect(status.branch).toBe("rowl/pr-488/statemachine"); expect(status.pr).toEqual({ number: 488, title: "Rebase this PR on latest main", @@ -594,7 +594,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("status returns merged PR state when latest PR was merged", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/status-merged-pr"]); @@ -632,7 +632,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("status prefers open PR when merged PR has newer updatedAt", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/status-open-over-merged"]); @@ -679,7 +679,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("status is resilient to gh lookup failures and returns pr null", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/status-no-gh"]); const remoteDir = yield* createBareRemote(); @@ -703,7 +703,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("creates a commit when working tree is dirty", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); fs.writeFileSync(path.join(repoDir, "README.md"), "hello\nworld\n"); @@ -727,7 +727,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("uses custom commit message when provided", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); fs.writeFileSync(path.join(repoDir, "README.md"), "hello\ncustom\n"); let generatedCount = 0; @@ -770,7 +770,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("commits only selected files when filePaths is provided", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); fs.writeFileSync(path.join(repoDir, "a.txt"), "file a\n"); fs.writeFileSync(path.join(repoDir, "b.txt"), "file b\n"); @@ -795,7 +795,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("creates feature branch, commits, and pushes with featureBranch option", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const remoteDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); @@ -845,7 +845,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("featureBranch uses custom commit message and derives branch name", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); fs.writeFileSync(path.join(repoDir, "README.md"), "hello\ncustom-feature\n"); let generatedCount = 0; @@ -888,7 +888,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("skips commit when there are no uncommitted changes", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const { manager } = yield* makeManager(); @@ -906,7 +906,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("featureBranch returns error when worktree is clean", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const { manager } = yield* makeManager(); @@ -925,7 +925,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("commits and pushes with upstream auto-setup when needed", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/stacked-flow"]); const remoteDir = yield* createBareRemote(); @@ -954,7 +954,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "pushes and creates PR from a no-upstream branch when local commits are ahead of base", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/no-upstream-pr"]); const remoteDir = yield* createBareRemote(); @@ -1003,7 +1003,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("skips push when branch is already up to date", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/up-to-date"]); const remoteDir = yield* createBareRemote(); @@ -1024,7 +1024,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("returns existing PR metadata for commit/push/pr action", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/existing-pr"]); const remoteDir = yield* createBareRemote(); @@ -1062,7 +1062,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "returns existing cross-repo PR metadata using the fork owner selector", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "statemachine"]); const forkDir = yield* createBareRemote(); @@ -1112,13 +1112,13 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "prefers owner-qualified selectors before bare branch names for cross-repo PRs", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "statemachine"]); const forkDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "fork-seed", forkDir]); yield* runGit(repoDir, ["push", "-u", "fork-seed", "statemachine"]); - yield* runGit(repoDir, ["checkout", "-b", "cut3/pr-142/statemachine"]); + yield* runGit(repoDir, ["checkout", "-b", "rowl/pr-142/statemachine"]); yield* runGit(repoDir, ["branch", "--set-upstream-to", "fork-seed/statemachine"]); yield* runGit(repoDir, [ "config", @@ -1129,7 +1129,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { const { manager, ghCalls } = yield* makeManager({ ghScenario: { prListByHeadSelector: { - "cut3/pr-142/statemachine": JSON.stringify([]), + "rowl/pr-142/statemachine": JSON.stringify([]), statemachine: JSON.stringify([ { number: 41, @@ -1174,13 +1174,13 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "stops probing head selectors after finding an existing PR", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "statemachine"]); const forkDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "fork-seed", forkDir]); yield* runGit(repoDir, ["push", "-u", "fork-seed", "statemachine"]); - yield* runGit(repoDir, ["checkout", "-b", "cut3/pr-142/statemachine"]); + yield* runGit(repoDir, ["checkout", "-b", "rowl/pr-142/statemachine"]); yield* runGit(repoDir, ["branch", "--set-upstream-to", "fork-seed/statemachine"]); yield* runGit(repoDir, [ "config", @@ -1201,7 +1201,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }, ]), "fork-seed:statemachine": JSON.stringify([]), - "cut3/pr-142/statemachine": JSON.stringify([]), + "rowl/pr-142/statemachine": JSON.stringify([]), statemachine: JSON.stringify([]), }, }, @@ -1226,7 +1226,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("creates PR when one does not already exist", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature-create-pr"]); const remoteDir = yield* createBareRemote(); @@ -1270,7 +1270,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("creates cross-repo PRs with the fork owner selector and default base branch", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const forkDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "fork-seed", forkDir]); @@ -1279,7 +1279,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { yield* runGit(repoDir, ["add", "changes.txt"]); yield* runGit(repoDir, ["commit", "-m", "Feature commit"]); yield* runGit(repoDir, ["push", "-u", "fork-seed", "statemachine"]); - yield* runGit(repoDir, ["checkout", "-b", "cut3/pr-91/statemachine"]); + yield* runGit(repoDir, ["checkout", "-b", "rowl/pr-91/statemachine"]); yield* runGit(repoDir, ["branch", "--set-upstream-to", "fork-seed/statemachine"]); yield* runGit(repoDir, [ "config", @@ -1329,7 +1329,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("rejects push/pr actions from detached HEAD", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "--detach", "HEAD"]); @@ -1347,7 +1347,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("surfaces missing gh binary errors", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/gh-missing"]); const remoteDir = yield* createBareRemote(); @@ -1376,7 +1376,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("surfaces gh auth errors with guidance", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/gh-auth"]); const remoteDir = yield* createBareRemote(); @@ -1405,7 +1405,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("resolves pull requests from #number references", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const { manager, ghCalls } = yield* makeManager({ @@ -1440,7 +1440,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("prepares pull request threads in local mode by checking out the PR branch", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/pr-local"]); fs.writeFileSync(path.join(repoDir, "local.txt"), "local\n"); @@ -1476,7 +1476,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("prepares pull request threads in worktree mode on the PR head branch", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const remoteDir = yield* createBareRemote(); yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); @@ -1521,7 +1521,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("preserves fork upstream tracking when preparing a worktree PR thread", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const originDir = yield* createBareRemote(); const forkDir = yield* createBareRemote(); @@ -1583,7 +1583,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("preserves fork upstream tracking when preparing a local PR thread", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const originDir = yield* createBareRemote(); const forkDir = yield* createBareRemote(); @@ -1636,7 +1636,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("derives fork repository identity from PR URL when GitHub omits nameWithOwner", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const originDir = yield* createBareRemote(); const forkDir = yield* createBareRemote(); @@ -1661,7 +1661,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { pullRequest: { number: 642, title: "fix: use commit as the default git action without origin", - url: "https://github.com/pingdotgg/cut3/pull/642", + url: "https://github.com/pingdotgg/rowl/pull/642", baseRefName: "main", headRefName: "fix/git-action-default-without-origin", state: "open", @@ -1669,7 +1669,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { headRepositoryOwnerLogin: "binbandit", }, repositoryCloneUrls: { - "binbandit/cut3": { + "binbandit/rowl": { url: forkDir, sshUrl: forkDir, }, @@ -1693,7 +1693,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("reuses an existing dedicated worktree for the PR head branch", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/pr-existing-worktree"]); fs.writeFileSync(path.join(repoDir, "existing.txt"), "existing\n"); @@ -1733,7 +1733,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "does not block fork PR worktree prep when the fork head branch collides with root main", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const originDir = yield* createBareRemote(); const forkDir = yield* createBareRemote(); @@ -1776,7 +1776,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { mode: "worktree", }); - expect(result.branch).toBe("cut3/pr-91/main"); + expect(result.branch).toBe("rowl/pr-91/main"); expect(result.worktreePath).not.toBeNull(); expect((yield* runGit(repoDir, ["branch", "--show-current"])).stdout.trim()).toBe("main"); expect((yield* runGit(repoDir, ["rev-parse", "main"])).stdout.trim()).toBe(mainBefore); @@ -1785,7 +1785,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "branch", "--show-current", ])).stdout.trim(), - ).toBe("cut3/pr-91/main"); + ).toBe("rowl/pr-91/main"); }), ); @@ -1793,7 +1793,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { "does not overwrite an existing local main branch when preparing a fork PR worktree", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const originDir = yield* createBareRemote(); const forkDir = yield* createBareRemote(); @@ -1837,7 +1837,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { mode: "worktree", }); - expect(result.branch).toBe("cut3/pr-92/main"); + expect(result.branch).toBe("rowl/pr-92/main"); expect((yield* runGit(repoDir, ["rev-parse", "main"])).stdout.trim()).toBe(localMainBefore); expect( (yield* runGit(result.worktreePath as string, [ @@ -1851,7 +1851,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("reuses an existing PR worktree and restores fork upstream tracking", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); const originDir = yield* createBareRemote(); const forkDir = yield* createBareRemote(); @@ -1907,7 +1907,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { it.effect("rejects worktree prep when the PR head branch is checked out in the main repo", () => Effect.gen(function* () { - const repoDir = yield* makeTempDir("cut3-git-manager-"); + const repoDir = yield* makeTempDir("rowl-git-manager-"); yield* initRepo(repoDir); yield* runGit(repoDir, ["checkout", "-b", "feature/pr-root-only"]); diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index f30adc6bd74..8c98a5aec5a 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -90,7 +90,7 @@ function resolvePullRequestWorktreeLocalBranchName( const sanitizedHeadBranch = sanitizeBranchFragment(pullRequest.headBranch).trim(); const suffix = sanitizedHeadBranch.length > 0 ? sanitizedHeadBranch : "head"; - return `cut3/pr-${pullRequest.number}/${suffix}`; + return `rowl/pr-${pullRequest.number}/${suffix}`; } function parseGitHubRepositoryNameWithOwnerFromRemoteUrl(url: string | null): string | null { @@ -750,7 +750,7 @@ export const makeGitManager = Effect.gen(function* () { diffPatch: limitContext(rangeContext.diffPatch, 60_000), }); - const bodyFile = path.join(tempDir, `cut3-pr-body-${process.pid}-${randomUUID()}.md`); + const bodyFile = path.join(tempDir, `rowl-pr-body-${process.pid}-${randomUUID()}.md`); yield* fileSystem .writeFileString(bodyFile, generated.body) .pipe( diff --git a/apps/server/src/git/Services/GitCore.ts b/apps/server/src/git/Services/GitCore.ts index 4146174ab62..cbd808158e0 100644 --- a/apps/server/src/git/Services/GitCore.ts +++ b/apps/server/src/git/Services/GitCore.ts @@ -221,5 +221,5 @@ export interface GitCoreShape { * GitCore - Service tag for low-level Git repository operations. */ export class GitCore extends ServiceMap.Service()( - "cut3/git/Services/GitCore", + "rowl/git/Services/GitCore", ) {} diff --git a/apps/server/src/git/Services/GitHubCli.ts b/apps/server/src/git/Services/GitHubCli.ts index d349dee01d2..b1ead636f91 100644 --- a/apps/server/src/git/Services/GitHubCli.ts +++ b/apps/server/src/git/Services/GitHubCli.ts @@ -99,5 +99,5 @@ export interface GitHubCliShape { * GitHubCli - Service tag for GitHub CLI process execution. */ export class GitHubCli extends ServiceMap.Service()( - "cut3/git/Services/GitHubCli", + "rowl/git/Services/GitHubCli", ) {} diff --git a/apps/server/src/git/Services/GitManager.ts b/apps/server/src/git/Services/GitManager.ts index 4bc61c81636..ef4f37c84b2 100644 --- a/apps/server/src/git/Services/GitManager.ts +++ b/apps/server/src/git/Services/GitManager.ts @@ -58,5 +58,5 @@ export interface GitManagerShape { * GitManager - Service tag for stacked Git workflow orchestration. */ export class GitManager extends ServiceMap.Service()( - "cut3/git/Services/GitManager", + "rowl/git/Services/GitManager", ) {} diff --git a/apps/server/src/git/Services/GitService.ts b/apps/server/src/git/Services/GitService.ts index 10cf8d23cf7..a815dadb24d 100644 --- a/apps/server/src/git/Services/GitService.ts +++ b/apps/server/src/git/Services/GitService.ts @@ -41,5 +41,5 @@ export interface GitServiceShape { * GitService - Service for Git command execution. */ export class GitService extends ServiceMap.Service()( - "cut3/git/Services/GitService", + "rowl/git/Services/GitService", ) {} diff --git a/apps/server/src/git/Services/TextGeneration.ts b/apps/server/src/git/Services/TextGeneration.ts index 2193efb0d86..ff5e70bd27f 100644 --- a/apps/server/src/git/Services/TextGeneration.ts +++ b/apps/server/src/git/Services/TextGeneration.ts @@ -90,5 +90,5 @@ export interface TextGenerationShape { * TextGeneration - Service tag for commit and PR text generation. */ export class TextGeneration extends ServiceMap.Service()( - "cut3/git/Services/TextGeneration", + "rowl/git/Services/TextGeneration", ) {} diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 3bd9ef6ca49..84a5dbe1205 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -25,7 +25,7 @@ const makeKeybindingsLayer = () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { join } = yield* Path.Path; - const dir = yield* fs.makeTempDirectoryScoped({ prefix: "cut3-server-config-test-" }); + const dir = yield* fs.makeTempDirectoryScoped({ prefix: "rowl-server-config-test-" }); const configPath = join(dir, "keybindings.json"); return { keybindingsConfigPath: configPath } as ServerConfigShape; }), diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 991b2292d96..ab363a7342c 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -75,7 +75,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, { key: "escape", command: "chat.interrupt", when: "!terminalFocus" }, - { key: "mod+shift+p", command: "commandPalette.toggle" }, + { key: "mod+k", command: "commandPalette.toggle" }, { key: "mod+o", command: "editor.openFavorite" }, ]; @@ -528,7 +528,7 @@ export interface KeybindingsShape { * Keybindings - Service tag for keybinding configuration operations. */ export class Keybindings extends ServiceMap.Service()( - "cut3/keybindings", + "rowl/keybindings", ) {} const makeKeybindings = Effect.gen(function* () { diff --git a/apps/server/src/kimiAcpManager.test.ts b/apps/server/src/kimiAcpManager.test.ts index 7280dc4eafc..42d6b9c716e 100644 --- a/apps/server/src/kimiAcpManager.test.ts +++ b/apps/server/src/kimiAcpManager.test.ts @@ -55,9 +55,9 @@ describe("kimiAcpManager model availability", () => { buildKimiCliArgs({ runtimeMode: "approval-required", model: "kimi-for-coding", - configFilePath: "/tmp/cut3-kimi/config.json", + configFilePath: "/tmp/rowl-kimi/config.json", }), - ).toEqual(["--config-file", "/tmp/cut3-kimi/config.json", "--model", "kimi-for-coding", "acp"]); + ).toEqual(["--config-file", "/tmp/rowl-kimi/config.json", "--model", "kimi-for-coding", "acp"]); }); it("overrides inherited Kimi env vars with the selected model and API key", () => { @@ -103,7 +103,7 @@ describe("kimiAcpManager model availability", () => { ).toEqual({ default_model: "kimi-k2-thinking", providers: { - "cut3-kimi": { + "rowl-kimi": { type: "kimi", base_url: "https://api.kimi.com/coding/v1", api_key: "sk-kimi-test", @@ -111,12 +111,12 @@ describe("kimiAcpManager model availability", () => { }, models: { "kimi-k2-thinking": { - provider: "cut3-kimi", + provider: "rowl-kimi", model: "kimi-k2-thinking", max_context_size: 262144, }, "kimi-for-coding": { - provider: "cut3-kimi", + provider: "rowl-kimi", model: "kimi-for-coding", max_context_size: 262144, }, diff --git a/apps/server/src/kimiAcpManager.ts b/apps/server/src/kimiAcpManager.ts index f50a8f90a25..5f561d6a8b2 100644 --- a/apps/server/src/kimiAcpManager.ts +++ b/apps/server/src/kimiAcpManager.ts @@ -30,6 +30,7 @@ import { mapToolKindToItemType, mapToolKindToRequestType, permissionDecisionFromOutcome, + providerLog, readResumeSessionId, summarizeToolContent, toMessage, @@ -120,7 +121,7 @@ export function buildKimiCliArgs(input: { ]; } -const KIMI_CODE_PROVIDER_ID = "cut3-kimi"; +const KIMI_CODE_PROVIDER_ID = "rowl-kimi"; const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"; const KIMI_DEFAULT_MODEL_ID = "kimi-for-coding"; const KIMI_DEFAULT_MAX_CONTEXT_SIZE = 262_144; @@ -200,7 +201,7 @@ function createKimiApiKeyConfigFile(input: { readonly apiKey: string; readonly m readonly dirPath: string; readonly filePath: string; } { - const dirPath = mkdtempSync(join(tmpdir(), "cut3-kimi-")); + const dirPath = mkdtempSync(join(tmpdir(), "rowl-kimi-")); const filePath = join(dirPath, "config.json"); try { chmodSync(dirPath, 0o700); @@ -278,7 +279,7 @@ export function normalizeKimiStartErrorMessage(input: { ) || (input.loginProbeOutput && isKimiLoginProbeUnauthenticated(input.loginProbeOutput)) ) { - return "Kimi Code CLI requires authentication. Run `kimi login`, or start `kimi` and run `/login`, or add a Kimi API key in CUT3 Settings and try again."; + return "Kimi Code CLI requires authentication. Run `kimi login`, or start `kimi` and run `/login`, or add a Kimi API key in Rowl Settings and try again."; } return input.rawMessage; @@ -858,6 +859,11 @@ export class KimiAcpManager extends EventEmitter { payload: context.session.model ? { model: context.session.model } : {}, }); + providerLog( + "Turn", + `Starting turn ${turnId} for thread ${input.threadId}, model: ${context.session.model ?? "default"}`, + ); + try { const result = await context.connection.prompt({ sessionId: context.acpSessionId, @@ -869,6 +875,8 @@ export class KimiAcpManager extends EventEmitter { ], }); + providerLog("Turn", `Turn ${turnId} completed with stopReason: ${result.stopReason}`); + this.emitRuntimeEvent({ ...this.createEventBase(context), turnId, @@ -937,7 +945,7 @@ export class KimiAcpManager extends EventEmitter { } async respondToUserInput(): Promise { - throw new Error("Kimi Code CLI does not expose structured user input requests in CUT3."); + throw new Error("Kimi Code CLI does not expose structured user input requests in Rowl."); } async stopSession(threadId: ThreadId): Promise { @@ -971,6 +979,7 @@ export class KimiAcpManager extends EventEmitter { } private async disposeContext(context: KimiSessionContext): Promise { + providerLog("Session", `Disposing session for thread ${context.session.threadId}`); context.stopping = true; this.resolvePendingApprovalsAsCancelled(context); try { @@ -979,13 +988,19 @@ export class KimiAcpManager extends EventEmitter { // Best-effort cancellation only. } - killChildTree(context.child); + await new Promise((resolve) => setTimeout(resolve, 500)); + + if (!context.child.killed) { + providerLog("Session", `Sending SIGKILL to child process ${context.child.pid}`); + killChildTree(context.child, "SIGKILL"); + } this.updateSession(context, { status: "closed", activeTurnId: undefined, }); this.deleteTrackedSession(context.session.threadId, context); cleanupKimiTempConfig(context.tempConfigDir); + providerLog("Session", `Session disposed for thread ${context.session.threadId}`); } async listSessions(): Promise> { diff --git a/apps/server/src/main.test.ts b/apps/server/src/main.test.ts index 903f9111063..1bf36f9c2ca 100644 --- a/apps/server/src/main.test.ts +++ b/apps/server/src/main.test.ts @@ -63,7 +63,7 @@ const testLayer = Layer.mergeAll( const runCli = ( args: ReadonlyArray, - env: Record = { CUT3_NO_BROWSER: "true" }, + env: Record = { ROWL_NO_BROWSER: "true" }, ) => { const uniqueStateDir = `/tmp/t3-cli-state-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; return Command.runWith(t3Cli, { version: "0.0.0-test" })(args).pipe( @@ -71,7 +71,7 @@ const runCli = ( ConfigProvider.layer( ConfigProvider.fromEnv({ env: { - CUT3_STATE_DIR: uniqueStateDir, + ROWL_STATE_DIR: uniqueStateDir, ...env, }, }), @@ -134,13 +134,13 @@ it.layer(testLayer)("server CLI command", (it) => { it.effect("uses env fallbacks when flags are not provided", () => Effect.gen(function* () { yield* runCli([], { - CUT3_MODE: "desktop", - CUT3_PORT: "4999", - CUT3_HOST: "100.88.10.4", - CUT3_STATE_DIR: "/tmp/t3-env-state", + ROWL_MODE: "desktop", + ROWL_PORT: "4999", + ROWL_HOST: "100.88.10.4", + ROWL_STATE_DIR: "/tmp/t3-env-state", VITE_DEV_SERVER_URL: "http://localhost:5173", - CUT3_NO_BROWSER: "true", - CUT3_AUTH_TOKEN: "env-token", + ROWL_NO_BROWSER: "true", + ROWL_AUTH_TOKEN: "env-token", }); assert.equal(start.mock.calls.length, 1); @@ -157,12 +157,12 @@ it.layer(testLayer)("server CLI command", (it) => { }), ); - it.effect("prefers --mode over CUT3_MODE", () => + it.effect("prefers --mode over ROWL_MODE", () => Effect.gen(function* () { findAvailablePort.mockImplementation((_preferred: number) => Effect.succeed(4666)); yield* runCli(["--mode", "web"], { - CUT3_MODE: "desktop", - CUT3_NO_BROWSER: "true", + ROWL_MODE: "desktop", + ROWL_NO_BROWSER: "true", }); assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); @@ -173,10 +173,10 @@ it.layer(testLayer)("server CLI command", (it) => { }), ); - it.effect("prefers --no-browser over CUT3_NO_BROWSER", () => + it.effect("prefers --no-browser over ROWL_NO_BROWSER", () => Effect.gen(function* () { yield* runCli(["--no-browser"], { - CUT3_NO_BROWSER: "false", + ROWL_NO_BROWSER: "false", }); assert.equal(start.mock.calls.length, 1); @@ -187,8 +187,8 @@ it.layer(testLayer)("server CLI command", (it) => { it.effect("opens a tokenized browser url when auth is enabled", () => Effect.gen(function* () { yield* runCli([], { - CUT3_NO_BROWSER: "false", - CUT3_AUTH_TOKEN: "env-token", + ROWL_NO_BROWSER: "false", + ROWL_AUTH_TOKEN: "env-token", }); assert.deepStrictEqual(openBrowser.mock.calls, [["http://127.0.0.1:3773/?token=env-token"]]); @@ -211,8 +211,8 @@ it.layer(testLayer)("server CLI command", (it) => { it.effect("uses fixed localhost defaults in desktop mode", () => Effect.gen(function* () { yield* runCli([], { - CUT3_MODE: "desktop", - CUT3_NO_BROWSER: "true", + ROWL_MODE: "desktop", + ROWL_NO_BROWSER: "true", }); assert.equal(findAvailablePort.mock.calls.length, 0); @@ -226,9 +226,9 @@ it.layer(testLayer)("server CLI command", (it) => { it.effect("accepts an ephemeral desktop port from the environment", () => Effect.gen(function* () { yield* runCli([], { - CUT3_MODE: "desktop", - CUT3_PORT: "0", - CUT3_NO_BROWSER: "true", + ROWL_MODE: "desktop", + ROWL_PORT: "0", + ROWL_NO_BROWSER: "true", }); assert.equal(findAvailablePort.mock.calls.length, 0); @@ -242,8 +242,8 @@ it.layer(testLayer)("server CLI command", (it) => { it.effect("allows overriding desktop host with --host", () => Effect.gen(function* () { yield* runCli(["--host", "0.0.0.0"], { - CUT3_MODE: "desktop", - CUT3_NO_BROWSER: "true", + ROWL_MODE: "desktop", + ROWL_NO_BROWSER: "true", }); assert.equal(start.mock.calls.length, 1); @@ -255,10 +255,10 @@ it.layer(testLayer)("server CLI command", (it) => { it.effect("supports CLI and env for bootstrap/log websocket toggles", () => Effect.gen(function* () { yield* runCli(["--auto-bootstrap-project-from-cwd"], { - CUT3_MODE: "desktop", - CUT3_LOG_WS_EVENTS: "false", - CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false", - CUT3_NO_BROWSER: "true", + ROWL_MODE: "desktop", + ROWL_LOG_WS_EVENTS: "false", + ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false", + ROWL_NO_BROWSER: "true", }); assert.equal(start.mock.calls.length, 1); diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 9fb1139b7f1..db6cd452b4a 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -71,7 +71,7 @@ export interface CliConfigShape { * CliConfig - Service tag for startup CLI/runtime helpers. */ export class CliConfig extends ServiceMap.Service()( - "cut3/main/CliConfig", + "rowl/main/CliConfig", ) { static readonly layer = Layer.effect( CliConfig, @@ -91,7 +91,7 @@ export class CliConfig extends ServiceMap.Service()( } const CliEnvConfig = Config.all({ - mode: Config.string("CUT3_MODE").pipe( + mode: Config.string("ROWL_MODE").pipe( Config.option, Config.map( Option.match({ @@ -100,26 +100,26 @@ const CliEnvConfig = Config.all({ }), ), ), - port: Config.number("CUT3_PORT").pipe( + port: Config.number("ROWL_PORT").pipe( Config.option, Config.map(Option.match({ onNone: () => undefined, onSome: (value) => value })), ), - host: Config.string("CUT3_HOST").pipe(Config.option, Config.map(Option.getOrUndefined)), - stateDir: Config.string("CUT3_STATE_DIR").pipe(Config.option, Config.map(Option.getOrUndefined)), + host: Config.string("ROWL_HOST").pipe(Config.option, Config.map(Option.getOrUndefined)), + stateDir: Config.string("ROWL_STATE_DIR").pipe(Config.option, Config.map(Option.getOrUndefined)), devUrl: Config.url("VITE_DEV_SERVER_URL").pipe(Config.option, Config.map(Option.getOrUndefined)), - noBrowser: Config.boolean("CUT3_NO_BROWSER").pipe( + noBrowser: Config.boolean("ROWL_NO_BROWSER").pipe( Config.option, Config.map(Option.getOrUndefined), ), - authToken: Config.string("CUT3_AUTH_TOKEN").pipe( + authToken: Config.string("ROWL_AUTH_TOKEN").pipe( Config.option, Config.map(Option.getOrUndefined), ), - autoBootstrapProjectFromCwd: Config.boolean("CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe( + autoBootstrapProjectFromCwd: Config.boolean("ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe( Config.option, Config.map(Option.getOrUndefined), ), - logWebSocketEvents: Config.boolean("CUT3_LOG_WS_EVENTS").pipe( + logWebSocketEvents: Config.boolean("ROWL_LOG_WS_EVENTS").pipe( Config.option, Config.map(Option.getOrUndefined), ), @@ -276,7 +276,7 @@ const makeServerProgram = (input: CliInput) => ? `http://${formatHostForUrl(config.host)}:${listeningPort}` : localUrl; const { authToken, devUrl, ...safeConfig } = config; - yield* Effect.logInfo("CUT3 running", { + yield* Effect.logInfo("Rowl running", { ...safeConfig, devUrl: devUrl?.toString(), authEnabled: Boolean(authToken), @@ -315,7 +315,7 @@ const hostFlag = Flag.string("host").pipe( Flag.optional, ); const stateDirFlag = Flag.string("state-dir").pipe( - Flag.withDescription("State directory path (equivalent to CUT3_STATE_DIR)."), + Flag.withDescription("State directory path (equivalent to ROWL_STATE_DIR)."), Flag.optional, ); const devUrlFlag = Flag.string("dev-url").pipe( @@ -340,7 +340,7 @@ const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-fro ); const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe( Flag.withDescription( - "Emit server-side logs for outbound WebSocket push traffic (equivalent to CUT3_LOG_WS_EVENTS).", + "Emit server-side logs for outbound WebSocket push traffic (equivalent to ROWL_LOG_WS_EVENTS).", ), Flag.withAlias("log-ws-events"), Flag.optional, @@ -357,6 +357,6 @@ export const t3Cli = Command.make("t3", { autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag, logWebSocketEvents: logWebSocketEventsFlag, }).pipe( - Command.withDescription("Run the CUT3 server."), + Command.withDescription("Run the Rowl server."), Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), ); diff --git a/apps/server/src/networking.test.ts b/apps/server/src/networking.test.ts index 51d02730580..4ce5cad4a28 100644 --- a/apps/server/src/networking.test.ts +++ b/apps/server/src/networking.test.ts @@ -84,7 +84,7 @@ describe("networking", () => { expect( isAllowedWebSocketOrigin({ - originHeader: "cut3://app", + originHeader: "rowl://app", allowedOrigins, allowMissingOrigin: true, allowNullOrigin: true, diff --git a/apps/server/src/networking.ts b/apps/server/src/networking.ts index 1d4dc9ccc56..8e5d38b704b 100644 --- a/apps/server/src/networking.ts +++ b/apps/server/src/networking.ts @@ -1,6 +1,6 @@ const DEFAULT_LOOPBACK_HOST = "127.0.0.1"; const DEFAULT_LOOPBACK_ORIGIN_HOSTS = ["localhost", "127.0.0.1", "::1"] as const; -const DESKTOP_APP_ORIGIN = "cut3://app"; +const DESKTOP_APP_ORIGIN = "rowl://app"; function normalizeHost(host: string): string { return host.replace(/^\[/, "").replace(/\]$/, "").toLowerCase(); diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts index 025aed51b55..4c09660f245 100644 --- a/apps/server/src/open.test.ts +++ b/apps/server/src/open.test.ts @@ -155,7 +155,7 @@ it.layer(NodeServices.layer)("launchDetached", (it) => { it.effect("rejects when command does not exist", () => Effect.gen(function* () { const result = yield* launchDetached({ - command: `cut3-no-such-command-${Date.now()}`, + command: `rowl-no-such-command-${Date.now()}`, args: [], }).pipe(Effect.result); assert.equal(result._tag, "Failure"); diff --git a/apps/server/src/open.ts b/apps/server/src/open.ts index 0fae3bb166d..a1f4d30ec9f 100644 --- a/apps/server/src/open.ts +++ b/apps/server/src/open.ts @@ -198,7 +198,7 @@ export interface OpenShape { /** * Open - Service tag for browser/editor launch operations. */ -export class Open extends ServiceMap.Service()("cut3/open") {} +export class Open extends ServiceMap.Service()("rowl/open") {} // ============================== // Implementations diff --git a/apps/server/src/opencodeAcpManager.test.ts b/apps/server/src/opencodeAcpManager.test.ts index b807291c30f..167a24f9e18 100644 --- a/apps/server/src/opencodeAcpManager.test.ts +++ b/apps/server/src/opencodeAcpManager.test.ts @@ -110,7 +110,7 @@ describe("opencodeAcpManager errors", () => { expect( normalizeOpenCodeStartErrorMessage("Missing environment variable: 'OPENROUTER_API_KEY'."), ).toBe( - "OpenCode provider config requires OPENROUTER_API_KEY. Add an OpenRouter API key in CUT3 Settings or export OPENROUTER_API_KEY before starting CUT3.", + "OpenCode provider config requires OPENROUTER_API_KEY. Add an OpenRouter API key in Rowl Settings or export OPENROUTER_API_KEY before starting Rowl.", ); }); diff --git a/apps/server/src/opencodeAcpManager.ts b/apps/server/src/opencodeAcpManager.ts index 25b30606531..fe85bd89969 100644 --- a/apps/server/src/opencodeAcpManager.ts +++ b/apps/server/src/opencodeAcpManager.ts @@ -28,6 +28,7 @@ import { mapToolKindToItemType, mapToolKindToRequestType, permissionDecisionFromOutcome, + providerLog, readResumeSessionId, summarizeToolContent, toMessage, @@ -98,11 +99,30 @@ export function isOpenCodeDefaultModel(model: string | null | undefined): boolea return model?.trim() === OPENCODE_DEFAULT_MODEL; } +const OPENROUTER_PROVIDER_ID_NORMALIZATIONS: Record = { + zai: "z-ai", +}; + +function normalizeOpenRouterProviderId(providerId: string): string { + return OPENROUTER_PROVIDER_ID_NORMALIZATIONS[providerId] ?? providerId; +} + function normalizeRequestedOpenCodeModel(model: string | null | undefined): string | undefined { const trimmed = model?.trim(); if (!trimmed || isOpenCodeDefaultModel(trimmed)) { return undefined; } + const slashIndex = trimmed.indexOf("/"); + if (slashIndex > 0) { + const providerId = trimmed.slice(0, slashIndex); + const modelId = trimmed.slice(slashIndex + 1); + const normalizedProviderId = normalizeOpenRouterProviderId(providerId); + if (normalizedProviderId !== providerId) { + const normalized = `${normalizedProviderId}/${modelId}`; + providerLog("Model", `Normalized provider ID: '${trimmed}' -> '${normalized}'`); + return normalized; + } + } return trimmed; } @@ -200,7 +220,7 @@ export function buildOpenCodeCliEnv(input: { export function normalizeOpenCodeStartErrorMessage(rawMessage: string): string { if (/missing environment variable:\s*['"]?OPENROUTER_API_KEY['"]?/i.test(rawMessage)) { - return "OpenCode provider config requires OPENROUTER_API_KEY. Add an OpenRouter API key in CUT3 Settings or export OPENROUTER_API_KEY before starting CUT3."; + return "OpenCode provider config requires OPENROUTER_API_KEY. Add an OpenRouter API key in Rowl Settings or export OPENROUTER_API_KEY before starting Rowl."; } if ( @@ -792,6 +812,15 @@ export class OpenCodeAcpManager extends EventEmitter { if (requestedModel && requestedModel !== context.session.model) { if (isOpenCodeModelAvailable(context.models, requestedModel)) { await this.setSessionModel(context, requestedModel); + } else { + const availableModelIds = readAvailableOpenCodeModelIds(context.models); + const modelListInfo = + availableModelIds.length > 0 + ? ` Available models: ${availableModelIds.join(", ")}.` + : " No models are currently available from OpenCode."; + throw new Error( + `OpenCode does not have access to model '${requestedModel}'.${modelListInfo} Select one of the available models or reconfigure your OpenCode provider.`, + ); } } @@ -802,6 +831,11 @@ export class OpenCodeAcpManager extends EventEmitter { activeTurnId: turnId, }); + providerLog( + "Turn", + `Starting turn ${turnId} for thread ${input.threadId}, model: ${context.session.model ?? "default"}`, + ); + this.emitRuntimeEvent({ ...this.createEventBase(context), turnId, @@ -820,6 +854,8 @@ export class OpenCodeAcpManager extends EventEmitter { ], }); + providerLog("Turn", `Turn ${turnId} completed with stopReason: ${result.stopReason}`); + this.emitRuntimeEvent({ ...this.createEventBase(context), turnId, @@ -890,7 +926,7 @@ export class OpenCodeAcpManager extends EventEmitter { } async respondToUserInput(): Promise { - throw new Error("OpenCode ACP does not expose structured user input requests in CUT3 yet."); + throw new Error("OpenCode ACP does not expose structured user input requests in Rowl yet."); } async stopSession(threadId: ThreadId): Promise { @@ -924,6 +960,7 @@ export class OpenCodeAcpManager extends EventEmitter { } private async disposeContext(context: OpenCodeSessionContext): Promise { + providerLog("Session", `Disposing session for thread ${context.session.threadId}`); context.stopping = true; this.resolvePendingApprovalsAsCancelled(context); try { @@ -932,12 +969,18 @@ export class OpenCodeAcpManager extends EventEmitter { // Best-effort cancellation only. } - killChildTree(context.child); + await new Promise((resolve) => setTimeout(resolve, 500)); + + if (!context.child.killed) { + providerLog("Session", `Sending SIGKILL to child process ${context.child.pid}`); + killChildTree(context.child, "SIGKILL"); + } this.updateSession(context, { status: "closed", activeTurnId: undefined, }); this.deleteTrackedSession(context.session.threadId, context); + providerLog("Session", `Session disposed for thread ${context.session.threadId}`); } async listSessions(): Promise> { diff --git a/apps/server/src/orchestration/Errors.ts b/apps/server/src/orchestration/Errors.ts index 1ea038e1d13..2de6c420ad8 100644 --- a/apps/server/src/orchestration/Errors.ts +++ b/apps/server/src/orchestration/Errors.ts @@ -121,3 +121,55 @@ export function toListenerCallbackError(listener: "read-model" | "domain-event") cause, }); } + +export class FeatureServiceError extends Schema.TaggedErrorClass()( + "FeatureServiceError", + { + operation: Schema.String, + detail: Schema.String, + cause: Schema.optional(Schema.Defect), + }, +) { + override get message(): string { + return `Feature service error in ${this.operation}: ${this.detail}`; + } +} + +export class GoalServiceError extends Schema.TaggedErrorClass()( + "GoalServiceError", + { + operation: Schema.String, + detail: Schema.String, + cause: Schema.optional(Schema.Defect), + }, +) { + override get message(): string { + return `Goal service error in ${this.operation}: ${this.detail}`; + } +} + +export class ContextServiceError extends Schema.TaggedErrorClass()( + "ContextServiceError", + { + operation: Schema.String, + detail: Schema.String, + cause: Schema.optional(Schema.Defect), + }, +) { + override get message(): string { + return `Context service error in ${this.operation}: ${this.detail}`; + } +} + +export class PMChatContextServiceError extends Schema.TaggedErrorClass()( + "PMChatContextServiceError", + { + operation: Schema.String, + detail: Schema.String, + cause: Schema.optional(Schema.Defect), + }, +) { + override get message(): string { + return `PM chat context service error in ${this.operation}: ${this.detail}`; + } +} diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index aadbf662fc3..cf6bf83520b 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -228,6 +228,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { id: ThreadId.makeUnsafe("thread-1"), projectId: asProjectId("project-1"), title: "Thread 1", + goal: null, model: "gpt-5-codex", interactionMode: "default", runtimeMode: "full-access", diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index c4ebc48894c..338400bb20e 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -537,6 +537,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { id: row.threadId, projectId: row.projectId, title: row.title, + goal: null, model: row.model, runtimeMode: row.runtimeMode, interactionMode: row.interactionMode, diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 0e9c8b59f82..fb88d29dde5 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -93,7 +93,7 @@ describe("ProviderCommandReactor", () => { >; }) { const now = new Date().toISOString(); - const stateDir = input?.stateDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "cut3-reactor-")); + const stateDir = input?.stateDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "rowl-reactor-")); const workspaceRoot = input?.workspaceRoot ?? "/tmp/provider-project"; fs.mkdirSync(workspaceRoot, { recursive: true }); createdStateDirs.add(stateDir); @@ -315,7 +315,7 @@ describe("ProviderCommandReactor", () => { }); it("injects workspace AGENTS.md instructions into provider turn input", async () => { - const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-reactor-workspace-")); + const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-reactor-workspace-")); fs.writeFileSync( path.join(workspaceRoot, "AGENTS.md"), ["# AGENTS.md", "", "Always mention the release checklist."].join("\n"), @@ -1455,4 +1455,105 @@ describe("ProviderCommandReactor", () => { expect(thread?.session?.threadId).toBe("thread-1"); expect(thread?.session?.activeTurnId).toBeNull(); }); + + describe("per-thread concurrency", () => { + it("control events are not serialized behind turn-start events", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-concurrent-ctrl-turn"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-ctrl"), + role: "user", + text: "turn with interrupt", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.sendTurn.mock.calls.length >= 1); + + const interruptCallCountBefore = harness.interruptTurn.mock.calls.length; + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.interrupt", + commandId: CommandId.makeUnsafe("cmd-concurrent-ctrl-interrupt"), + threadId: ThreadId.makeUnsafe("thread-1"), + turnId: undefined, + createdAt: now, + }), + ); + + await waitFor(() => harness.interruptTurn.mock.calls.length > interruptCallCountBefore, 3000); + + expect(harness.interruptTurn.mock.calls.length).toBeGreaterThan(0); + }); + + it("different threads' turn-start events both reach sendTurn", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-2"), + threadId: ThreadId.makeUnsafe("thread-2"), + projectId: asProjectId("project-1"), + title: "Thread 2", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + }), + ); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-concurrent-t1"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-t1"), + role: "user", + text: "turn thread 1", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-concurrent-t2"), + threadId: ThreadId.makeUnsafe("thread-2"), + message: { + messageId: asMessageId("user-message-t2"), + role: "user", + text: "turn thread 2", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.sendTurn.mock.calls.length >= 2, 5000); + + expect(harness.sendTurn.mock.calls.length).toBe(2); + }); + }); }); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index fe8d0b81e50..5f2cd3d49d9 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -15,8 +15,20 @@ import { type TurnId, } from "@t3tools/contracts"; import { isCodexOpenRouterModel } from "@t3tools/shared/model"; -import { Cache, Cause, Duration, Effect, Layer, Option, Schema, Stream } from "effect"; -import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; +import { + Cache, + Cause, + Duration, + Effect, + Fiber, + Layer, + Option, + Queue, + Ref, + Schema, + Stream, +} from "effect"; +import * as Semaphore from "effect/Semaphore"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; import { GitCore } from "../../git/Services/GitCore.ts"; @@ -162,7 +174,7 @@ const serverCommandId = (tag: string): CommandId => const HANDLED_TURN_START_KEY_MAX = 10_000; const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; -const WORKTREE_BRANCH_PREFIX = "cut3"; +const WORKTREE_BRANCH_PREFIX = "rowl"; const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`); function toErrorMessage(error: unknown): string { @@ -241,6 +253,38 @@ const make = Effect.gen(function* () { ); const threadProviderOptions = new Map(); + const perThreadSemaphores = new Map(); + + const getSemaphoreForThread = (threadId: ThreadId): Semaphore.Semaphore => { + let sem = perThreadSemaphores.get(threadId); + if (!sem) { + sem = Effect.runSync(Semaphore.make(1)); + perThreadSemaphores.set(threadId, sem); + } + return sem; + }; + + type Lane = { + readonly queue: Queue.Queue; + readonly fiber: Fiber.Fiber; + }; + + const perThreadLanesRef = yield* Ref.make>(new Map()); + + const getOrCreateLane = (threadId: ThreadId) => + Effect.gen(function* () { + const lanes = yield* Ref.get(perThreadLanesRef); + const existing = lanes.get(threadId); + if (existing) return existing; + + const queue = yield* Queue.unbounded(); + const fiber = yield* Effect.forkScoped( + Effect.forever(Queue.take(queue).pipe(Effect.flatMap(processDomainEventSafely))), + ); + const lane: Lane = { queue, fiber }; + yield* Ref.set(perThreadLanesRef, new Map(lanes).set(threadId, lane)); + return lane; + }); const appendProviderFailureActivity = (input: { readonly threadId: ThreadId; @@ -748,63 +792,66 @@ const make = Effect.gen(function* () { .pipe(Effect.catch(() => Effect.void)); } - yield* sendTurnForThread({ - threadId: event.payload.threadId, - messageText: message.text, - ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), - ...(event.payload.provider !== undefined ? { provider: event.payload.provider } : {}), - ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), - ...(event.payload.modelOptions !== undefined - ? { modelOptions: event.payload.modelOptions } - : {}), - ...(providerOptions !== undefined ? { providerOptions } : {}), - ...(event.payload.skills !== undefined ? { skills: event.payload.skills } : {}), - interactionMode: event.payload.interactionMode, - createdAt: event.payload.createdAt, - }).pipe( - Effect.tap(() => - Effect.logInfo("provider command reactor completed turn-start send", { - threadId: event.payload.threadId, - provider: event.payload.provider ?? null, - model: event.payload.model ?? null, - }), - ), - Effect.catchCause((cause) => - Effect.gen(function* () { - const error = Cause.squash(cause); - const detail = toErrorMessage(error); - const sessionProvider = - thread.session?.providerName === "codex" || - thread.session?.providerName === "copilot" || - thread.session?.providerName === "kimi" || - thread.session?.providerName === "opencode" || - thread.session?.providerName === "pi" - ? thread.session.providerName - : null; - const provider = event.payload.provider ?? sessionProvider; - - yield* appendProviderFailureActivity({ - threadId: event.payload.threadId, - kind: "provider.turn.start.failed", - summary: "Provider turn start failed", - detail, - turnId: thread.latestTurn?.turnId ?? null, - createdAt: event.payload.createdAt, - }); - yield* setProviderFailureSession({ + const sem = getSemaphoreForThread(event.payload.threadId); + yield* sem.withPermits(1)( + sendTurnForThread({ + threadId: event.payload.threadId, + messageText: message.text, + ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), + ...(event.payload.provider !== undefined ? { provider: event.payload.provider } : {}), + ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), + ...(event.payload.modelOptions !== undefined + ? { modelOptions: event.payload.modelOptions } + : {}), + ...(providerOptions !== undefined ? { providerOptions } : {}), + ...(event.payload.skills !== undefined ? { skills: event.payload.skills } : {}), + interactionMode: event.payload.interactionMode, + createdAt: event.payload.createdAt, + }).pipe( + Effect.tap(() => + Effect.logInfo("provider command reactor completed turn-start send", { threadId: event.payload.threadId, - provider, - runtimeMode: thread.runtimeMode, - lastError: detail, - createdAt: event.payload.createdAt, - ...(thread.session?.startedAt !== undefined - ? { startedAt: thread.session.startedAt } - : {}), - ...(thread.session?.tokenUsage !== undefined - ? { tokenUsage: thread.session.tokenUsage } - : {}), - }); - }), + provider: event.payload.provider ?? null, + model: event.payload.model ?? null, + }), + ), + Effect.catchCause((cause) => + Effect.gen(function* () { + const error = Cause.squash(cause); + const detail = toErrorMessage(error); + const sessionProvider = + thread.session?.providerName === "codex" || + thread.session?.providerName === "copilot" || + thread.session?.providerName === "kimi" || + thread.session?.providerName === "opencode" || + thread.session?.providerName === "pi" + ? thread.session.providerName + : null; + const provider = event.payload.provider ?? sessionProvider; + + yield* appendProviderFailureActivity({ + threadId: event.payload.threadId, + kind: "provider.turn.start.failed", + summary: "Provider turn start failed", + detail, + turnId: thread.latestTurn?.turnId ?? null, + createdAt: event.payload.createdAt, + }); + yield* setProviderFailureSession({ + threadId: event.payload.threadId, + provider, + runtimeMode: thread.runtimeMode, + lastError: detail, + createdAt: event.payload.createdAt, + ...(thread.session?.startedAt !== undefined + ? { startedAt: thread.session.startedAt } + : {}), + ...(thread.session?.tokenUsage !== undefined + ? { tokenUsage: thread.session.tokenUsage } + : {}), + }); + }), + ), ), ); }); @@ -999,8 +1046,6 @@ const make = Effect.gen(function* () { }), ); - const worker = yield* makeDrainableWorker(processDomainEventSafely); - const start: ProviderCommandReactorShape["start"] = Effect.forkScoped( Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { if ( @@ -1014,13 +1059,27 @@ const make = Effect.gen(function* () { return Effect.void; } - return worker.enqueue(event); + if (event.type === "thread.turn-start-requested") { + return getOrCreateLane(event.payload.threadId).pipe( + Effect.flatMap((lane) => Queue.offer(lane.queue, event)), + Effect.asVoid, + ); + } + + return Effect.forkScoped(processDomainEventSafely(event)); }), ).pipe(Effect.asVoid); + const drain = Effect.gen(function* () { + const lanes = yield* Ref.get(perThreadLanesRef); + const laneEntries = Array.from(lanes.entries()); + yield* Effect.forEach(laneEntries, ([, lane]) => Queue.shutdown(lane.queue)); + yield* Effect.forEach(laneEntries, ([, lane]) => Fiber.interrupt(lane.fiber)); + }); + return { start, - drain: worker.drain, + drain, } satisfies ProviderCommandReactorShape; }); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 03555690c03..c54a9c439be 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -35,7 +35,7 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120); const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000; const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120); const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; -const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.CUT3_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0"; +const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.ROWL_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0"; type TurnStartRequestedDomainEvent = Extract< OrchestrationEvent, diff --git a/apps/server/src/orchestration/Services/CheckpointReactor.ts b/apps/server/src/orchestration/Services/CheckpointReactor.ts index dacb4e0e21a..077fe0a1099 100644 --- a/apps/server/src/orchestration/Services/CheckpointReactor.ts +++ b/apps/server/src/orchestration/Services/CheckpointReactor.ts @@ -37,4 +37,4 @@ export interface CheckpointReactorShape { export class CheckpointReactor extends ServiceMap.Service< CheckpointReactor, CheckpointReactorShape ->()("cut3/orchestration/Services/CheckpointReactor") {} +>()("rowl/orchestration/Services/CheckpointReactor") {} diff --git a/apps/server/src/orchestration/Services/ContextService.ts b/apps/server/src/orchestration/Services/ContextService.ts new file mode 100644 index 00000000000..ada394d67c3 --- /dev/null +++ b/apps/server/src/orchestration/Services/ContextService.ts @@ -0,0 +1,67 @@ +/** + * ContextService - Service interface for context node management. + * + * Manages context nodes including CRUD operations, compression, + * and context budget tracking. + * + * @module ContextService + */ +import type { + CompressContextNodeInput, + CompressContextNodeResult, + ContextBudget, + CreateContextNodeInput, + CreateContextNodeResult, + DeleteContextNodeInput, + DeleteContextNodeResult, + GetContextNodeInput, + GetContextNodeResult, + ListContextNodesByProjectInput, + ListContextNodesByProjectResult, + ListContextNodesByThreadInput, + ListContextNodesByThreadResult, + RestoreContextNodeInput, + RestoreContextNodeResult, +} from "@t3tools/contracts"; +import { ServiceMap } from "effect"; +import type { Effect } from "effect"; + +import type { ContextServiceError } from "../Errors.ts"; + +export interface ContextServiceShape { + readonly createContextNode: ( + input: CreateContextNodeInput, + ) => Effect.Effect; + + readonly getContextNode: ( + input: GetContextNodeInput, + ) => Effect.Effect; + + readonly listContextNodesByProject: ( + input: ListContextNodesByProjectInput, + ) => Effect.Effect; + + readonly listContextNodesByThread: ( + input: ListContextNodesByThreadInput, + ) => Effect.Effect; + + readonly compressContextNode: ( + input: CompressContextNodeInput, + ) => Effect.Effect; + + readonly restoreContextNode: ( + input: RestoreContextNodeInput, + ) => Effect.Effect; + + readonly deleteContextNode: ( + input: DeleteContextNodeInput, + ) => Effect.Effect; + + readonly getContextBudget: ( + projectId: string, + ) => Effect.Effect; +} + +export class ContextService extends ServiceMap.Service()( + "rowl/orchestration/Services/ContextService", +) {} diff --git a/apps/server/src/orchestration/Services/FeatureService.ts b/apps/server/src/orchestration/Services/FeatureService.ts new file mode 100644 index 00000000000..6928acc33a7 --- /dev/null +++ b/apps/server/src/orchestration/Services/FeatureService.ts @@ -0,0 +1,56 @@ +/** + * FeatureService - Service interface for feature management. + * + * Manages features within projects including CRUD operations and stage + * transitions. + * + * @module FeatureService + */ +import type { + CreateFeatureInput, + CreateFeatureResult, + DeleteFeatureInput, + DeleteFeatureResult, + GetFeatureInput, + GetFeatureResult, + ListFeaturesByProjectInput, + ListFeaturesByProjectResult, + UpdateFeatureInput, + UpdateFeatureResult, + UpdateFeatureStageInput, + UpdateFeatureStageResult, +} from "@t3tools/contracts"; +import { ServiceMap } from "effect"; +import type { Effect } from "effect"; + +import type { FeatureServiceError } from "../Errors.ts"; + +export interface FeatureServiceShape { + readonly createFeature: ( + input: CreateFeatureInput, + ) => Effect.Effect; + + readonly getFeature: ( + input: GetFeatureInput, + ) => Effect.Effect; + + readonly listFeaturesByProject: ( + input: ListFeaturesByProjectInput, + ) => Effect.Effect; + + readonly updateFeature: ( + input: UpdateFeatureInput, + ) => Effect.Effect; + + readonly updateFeatureStage: ( + input: UpdateFeatureStageInput, + ) => Effect.Effect; + + readonly deleteFeature: ( + input: DeleteFeatureInput, + ) => Effect.Effect; +} + +export class FeatureService extends ServiceMap.Service()( + "rowl/orchestration/Services/FeatureService", +) {} diff --git a/apps/server/src/orchestration/Services/GoalsService.ts b/apps/server/src/orchestration/Services/GoalsService.ts new file mode 100644 index 00000000000..8ab19650d0a --- /dev/null +++ b/apps/server/src/orchestration/Services/GoalsService.ts @@ -0,0 +1,66 @@ +/** + * GoalsService - Service interface for goal management. + * + * Manages project goals including CRUD operations, linking threads, + * and setting main goals. + * + * @module GoalsService + */ +import type { + CreateGoalInput, + CreateGoalResult, + DeleteGoalInput, + DeleteGoalResult, + GetGoalInput, + GetGoalResult, + LinkThreadToGoalInput, + LinkThreadToGoalResult, + ListGoalsByProjectInput, + ListGoalsByProjectResult, + SetMainGoalInput, + SetMainGoalResult, + UnlinkThreadFromGoalInput, + UnlinkThreadFromGoalResult, + UpdateGoalTextInput, + UpdateGoalTextResult, +} from "@t3tools/contracts"; +import { ServiceMap } from "effect"; +import type { Effect } from "effect"; + +import type { GoalServiceError } from "../Errors.ts"; + +export interface GoalsServiceShape { + readonly createGoal: ( + input: CreateGoalInput, + ) => Effect.Effect; + + readonly getGoal: (input: GetGoalInput) => Effect.Effect; + + readonly listGoalsByProject: ( + input: ListGoalsByProjectInput, + ) => Effect.Effect; + + readonly setMainGoal: ( + input: SetMainGoalInput, + ) => Effect.Effect; + + readonly linkThreadToGoal: ( + input: LinkThreadToGoalInput, + ) => Effect.Effect; + + readonly unlinkThreadFromGoal: ( + input: UnlinkThreadFromGoalInput, + ) => Effect.Effect; + + readonly updateGoalText: ( + input: UpdateGoalTextInput, + ) => Effect.Effect; + + readonly deleteGoal: ( + input: DeleteGoalInput, + ) => Effect.Effect; +} + +export class GoalsService extends ServiceMap.Service()( + "rowl/orchestration/Services/GoalsService", +) {} diff --git a/apps/server/src/orchestration/Services/OrchestrationEngine.ts b/apps/server/src/orchestration/Services/OrchestrationEngine.ts index 80f7e52153b..88f449c35a7 100644 --- a/apps/server/src/orchestration/Services/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Services/OrchestrationEngine.ts @@ -77,4 +77,4 @@ export interface OrchestrationEngineShape { export class OrchestrationEngineService extends ServiceMap.Service< OrchestrationEngineService, OrchestrationEngineShape ->()("cut3/orchestration/Services/OrchestrationEngine/OrchestrationEngineService") {} +>()("rowl/orchestration/Services/OrchestrationEngine/OrchestrationEngineService") {} diff --git a/apps/server/src/orchestration/Services/OrchestrationReactor.ts b/apps/server/src/orchestration/Services/OrchestrationReactor.ts index 909da634b5c..dbdcfb7e777 100644 --- a/apps/server/src/orchestration/Services/OrchestrationReactor.ts +++ b/apps/server/src/orchestration/Services/OrchestrationReactor.ts @@ -28,4 +28,4 @@ export interface OrchestrationReactorShape { export class OrchestrationReactor extends ServiceMap.Service< OrchestrationReactor, OrchestrationReactorShape ->()("cut3/orchestration/Services/OrchestrationReactor") {} +>()("rowl/orchestration/Services/OrchestrationReactor") {} diff --git a/apps/server/src/orchestration/Services/PMChatContextService.ts b/apps/server/src/orchestration/Services/PMChatContextService.ts new file mode 100644 index 00000000000..63c8cac9111 --- /dev/null +++ b/apps/server/src/orchestration/Services/PMChatContextService.ts @@ -0,0 +1,37 @@ +/** + * PMChatContextService - Service interface for PM chat context aggregation. + * + * Provides aggregated project context for PM chat including all threads, + * features, goals, and context nodes. + * + * @module PMChatContextService + */ +import { ContextNode, Feature, Goal, ProjectId, ThreadId } from "@t3tools/contracts"; +import { ServiceMap } from "effect"; +import type { Effect } from "effect"; + +import type { PMChatContextServiceError } from "../Errors.ts"; + +export interface PMChatProjectContext { + readonly projectId: ProjectId; + readonly threads: ReadonlyArray<{ + threadId: ThreadId; + title: string; + goalStatement: string; + status: string; + }>; + readonly features: ReadonlyArray; + readonly goals: ReadonlyArray; + readonly contextNodes: ReadonlyArray; +} + +export interface PMChatContextServiceShape { + readonly getProjectContext: ( + projectId: ProjectId, + ) => Effect.Effect; +} + +export class PMChatContextService extends ServiceMap.Service< + PMChatContextService, + PMChatContextServiceShape +>()("rowl/orchestration/Services/PMChatContextService") {} diff --git a/apps/server/src/orchestration/Services/ProjectionPipeline.ts b/apps/server/src/orchestration/Services/ProjectionPipeline.ts index 43109bb812d..0bdce45a3a2 100644 --- a/apps/server/src/orchestration/Services/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Services/ProjectionPipeline.ts @@ -39,4 +39,4 @@ export interface OrchestrationProjectionPipelineShape { export class OrchestrationProjectionPipeline extends ServiceMap.Service< OrchestrationProjectionPipeline, OrchestrationProjectionPipelineShape ->()("cut3/orchestration/Services/ProjectionPipeline/OrchestrationProjectionPipeline") {} +>()("rowl/orchestration/Services/ProjectionPipeline/OrchestrationProjectionPipeline") {} diff --git a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts index 6d507d5dbb9..164ce147ea5 100644 --- a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts @@ -31,4 +31,4 @@ export interface ProjectionSnapshotQueryShape { export class ProjectionSnapshotQuery extends ServiceMap.Service< ProjectionSnapshotQuery, ProjectionSnapshotQueryShape ->()("cut3/orchestration/Services/ProjectionSnapshotQuery") {} +>()("rowl/orchestration/Services/ProjectionSnapshotQuery") {} diff --git a/apps/server/src/orchestration/Services/ProviderCommandReactor.ts b/apps/server/src/orchestration/Services/ProviderCommandReactor.ts index b7699b90c08..36716d0cf45 100644 --- a/apps/server/src/orchestration/Services/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Services/ProviderCommandReactor.ts @@ -37,4 +37,4 @@ export interface ProviderCommandReactorShape { export class ProviderCommandReactor extends ServiceMap.Service< ProviderCommandReactor, ProviderCommandReactorShape ->()("cut3/orchestration/Services/ProviderCommandReactor") {} +>()("rowl/orchestration/Services/ProviderCommandReactor") {} diff --git a/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts index 981aa5e7cd6..47565f1051b 100644 --- a/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts @@ -37,4 +37,4 @@ export interface ProviderRuntimeIngestionShape { export class ProviderRuntimeIngestionService extends ServiceMap.Service< ProviderRuntimeIngestionService, ProviderRuntimeIngestionShape ->()("cut3/orchestration/Services/ProviderRuntimeIngestion/ProviderRuntimeIngestionService") {} +>()("rowl/orchestration/Services/ProviderRuntimeIngestion/ProviderRuntimeIngestionService") {} diff --git a/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts b/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts index 7299cfbbcd6..f5cc9409d78 100644 --- a/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts +++ b/apps/server/src/orchestration/Services/RuntimeReceiptBus.ts @@ -46,4 +46,4 @@ export interface RuntimeReceiptBusShape { export class RuntimeReceiptBus extends ServiceMap.Service< RuntimeReceiptBus, RuntimeReceiptBusShape ->()("cut3/orchestration/Services/RuntimeReceiptBus") {} +>()("rowl/orchestration/Services/RuntimeReceiptBus") {} diff --git a/apps/server/src/orchestration/commandInvariants.test.ts b/apps/server/src/orchestration/commandInvariants.test.ts index f95e4db754b..aab3aa53931 100644 --- a/apps/server/src/orchestration/commandInvariants.test.ts +++ b/apps/server/src/orchestration/commandInvariants.test.ts @@ -50,6 +50,7 @@ const readModel: OrchestrationReadModel = { id: ThreadId.makeUnsafe("thread-1"), projectId: ProjectId.makeUnsafe("project-a"), title: "Thread A", + goal: null, model: "gpt-5-codex", interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, runtimeMode: "full-access", @@ -69,6 +70,7 @@ const readModel: OrchestrationReadModel = { id: ThreadId.makeUnsafe("thread-2"), projectId: ProjectId.makeUnsafe("project-b"), title: "Thread B", + goal: null, model: "gpt-5-codex", interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, runtimeMode: "full-access", diff --git a/apps/server/src/persistence/NodeSqliteClient.ts b/apps/server/src/persistence/NodeSqliteClient.ts index fbc6a51bc7b..0bde7800879 100644 --- a/apps/server/src/persistence/NodeSqliteClient.ts +++ b/apps/server/src/persistence/NodeSqliteClient.ts @@ -33,7 +33,7 @@ export type TypeId = "~local/sqlite-node/SqliteClient"; * SqliteClient - Effect service tag for the sqlite SQL client. */ export const SqliteClient = ServiceMap.Service( - "cut3/persistence/NodeSqliteClient", + "rowl/persistence/NodeSqliteClient", ); export interface SqliteClientConfig { diff --git a/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts b/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts index f12492a1ad9..583551bddd6 100644 --- a/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts +++ b/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts @@ -67,5 +67,5 @@ export class OrchestrationCommandReceiptRepository extends ServiceMap.Service< OrchestrationCommandReceiptRepository, OrchestrationCommandReceiptRepositoryShape >()( - "cut3/persistence/Services/OrchestrationCommandReceipts/OrchestrationCommandReceiptRepository", + "rowl/persistence/Services/OrchestrationCommandReceipts/OrchestrationCommandReceiptRepository", ) {} diff --git a/apps/server/src/persistence/Services/OrchestrationEventStore.ts b/apps/server/src/persistence/Services/OrchestrationEventStore.ts index f4c2e51ddb4..ece88ecd9c0 100644 --- a/apps/server/src/persistence/Services/OrchestrationEventStore.ts +++ b/apps/server/src/persistence/Services/OrchestrationEventStore.ts @@ -67,4 +67,4 @@ export interface OrchestrationEventStoreShape { export class OrchestrationEventStore extends ServiceMap.Service< OrchestrationEventStore, OrchestrationEventStoreShape ->()("cut3/persistence/Services/OrchestrationEventStore") {} +>()("rowl/persistence/Services/OrchestrationEventStore") {} diff --git a/apps/server/src/persistence/Services/ProjectionCheckpoints.ts b/apps/server/src/persistence/Services/ProjectionCheckpoints.ts index ad21b849032..f4bb05a168d 100644 --- a/apps/server/src/persistence/Services/ProjectionCheckpoints.ts +++ b/apps/server/src/persistence/Services/ProjectionCheckpoints.ts @@ -90,4 +90,4 @@ export interface ProjectionCheckpointRepositoryShape { export class ProjectionCheckpointRepository extends ServiceMap.Service< ProjectionCheckpointRepository, ProjectionCheckpointRepositoryShape ->()("cut3/persistence/Services/ProjectionCheckpoints/ProjectionCheckpointRepository") {} +>()("rowl/persistence/Services/ProjectionCheckpoints/ProjectionCheckpointRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts b/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts index 61d34affcba..f1a19ecfe3a 100644 --- a/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts +++ b/apps/server/src/persistence/Services/ProjectionPendingApprovals.ts @@ -88,4 +88,4 @@ export interface ProjectionPendingApprovalRepositoryShape { export class ProjectionPendingApprovalRepository extends ServiceMap.Service< ProjectionPendingApprovalRepository, ProjectionPendingApprovalRepositoryShape ->()("cut3/persistence/Services/ProjectionPendingApprovals/ProjectionPendingApprovalRepository") {} +>()("rowl/persistence/Services/ProjectionPendingApprovals/ProjectionPendingApprovalRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionProjects.ts b/apps/server/src/persistence/Services/ProjectionProjects.ts index 56101443ae7..5baf8af48f8 100644 --- a/apps/server/src/persistence/Services/ProjectionProjects.ts +++ b/apps/server/src/persistence/Services/ProjectionProjects.ts @@ -76,4 +76,4 @@ export interface ProjectionProjectRepositoryShape { export class ProjectionProjectRepository extends ServiceMap.Service< ProjectionProjectRepository, ProjectionProjectRepositoryShape ->()("cut3/persistence/Services/ProjectionProjects/ProjectionProjectRepository") {} +>()("rowl/persistence/Services/ProjectionProjects/ProjectionProjectRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionState.ts b/apps/server/src/persistence/Services/ProjectionState.ts index 3e15d9b69a7..7d93f33063b 100644 --- a/apps/server/src/persistence/Services/ProjectionState.ts +++ b/apps/server/src/persistence/Services/ProjectionState.ts @@ -61,4 +61,4 @@ export interface ProjectionStateRepositoryShape { export class ProjectionStateRepository extends ServiceMap.Service< ProjectionStateRepository, ProjectionStateRepositoryShape ->()("cut3/persistence/Services/ProjectionState/ProjectionStateRepository") {} +>()("rowl/persistence/Services/ProjectionState/ProjectionStateRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionThreadActivities.ts b/apps/server/src/persistence/Services/ProjectionThreadActivities.ts index 65f202c9287..58e8eb41bfe 100644 --- a/apps/server/src/persistence/Services/ProjectionThreadActivities.ts +++ b/apps/server/src/persistence/Services/ProjectionThreadActivities.ts @@ -80,4 +80,4 @@ export interface ProjectionThreadActivityRepositoryShape { export class ProjectionThreadActivityRepository extends ServiceMap.Service< ProjectionThreadActivityRepository, ProjectionThreadActivityRepositoryShape ->()("cut3/persistence/Services/ProjectionThreadActivities/ProjectionThreadActivityRepository") {} +>()("rowl/persistence/Services/ProjectionThreadActivities/ProjectionThreadActivityRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionThreadMessages.ts b/apps/server/src/persistence/Services/ProjectionThreadMessages.ts index 7c9be6363c6..d7c233d01be 100644 --- a/apps/server/src/persistence/Services/ProjectionThreadMessages.ts +++ b/apps/server/src/persistence/Services/ProjectionThreadMessages.ts @@ -78,4 +78,4 @@ export interface ProjectionThreadMessageRepositoryShape { export class ProjectionThreadMessageRepository extends ServiceMap.Service< ProjectionThreadMessageRepository, ProjectionThreadMessageRepositoryShape ->()("cut3/persistence/Services/ProjectionThreadMessages/ProjectionThreadMessageRepository") {} +>()("rowl/persistence/Services/ProjectionThreadMessages/ProjectionThreadMessageRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts b/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts index 29e5cedf83c..400bda78297 100644 --- a/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts +++ b/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts @@ -48,5 +48,5 @@ export class ProjectionThreadProposedPlanRepository extends ServiceMap.Service< ProjectionThreadProposedPlanRepository, ProjectionThreadProposedPlanRepositoryShape >()( - "cut3/persistence/Services/ProjectionThreadProposedPlans/ProjectionThreadProposedPlanRepository", + "rowl/persistence/Services/ProjectionThreadProposedPlans/ProjectionThreadProposedPlanRepository", ) {} diff --git a/apps/server/src/persistence/Services/ProjectionThreadSessions.ts b/apps/server/src/persistence/Services/ProjectionThreadSessions.ts index 35b0f119c82..e7746541ed3 100644 --- a/apps/server/src/persistence/Services/ProjectionThreadSessions.ts +++ b/apps/server/src/persistence/Services/ProjectionThreadSessions.ts @@ -73,4 +73,4 @@ export interface ProjectionThreadSessionRepositoryShape { export class ProjectionThreadSessionRepository extends ServiceMap.Service< ProjectionThreadSessionRepository, ProjectionThreadSessionRepositoryShape ->()("cut3/persistence/Services/ProjectionThreadSessions/ProjectionThreadSessionRepository") {} +>()("rowl/persistence/Services/ProjectionThreadSessions/ProjectionThreadSessionRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionThreads.ts b/apps/server/src/persistence/Services/ProjectionThreads.ts index d182c1458e3..dba69faf14c 100644 --- a/apps/server/src/persistence/Services/ProjectionThreads.ts +++ b/apps/server/src/persistence/Services/ProjectionThreads.ts @@ -91,4 +91,4 @@ export interface ProjectionThreadRepositoryShape { export class ProjectionThreadRepository extends ServiceMap.Service< ProjectionThreadRepository, ProjectionThreadRepositoryShape ->()("cut3/persistence/Services/ProjectionThreads/ProjectionThreadRepository") {} +>()("rowl/persistence/Services/ProjectionThreads/ProjectionThreadRepository") {} diff --git a/apps/server/src/persistence/Services/ProjectionTurns.ts b/apps/server/src/persistence/Services/ProjectionTurns.ts index b2ee3402b08..fc9d317ce63 100644 --- a/apps/server/src/persistence/Services/ProjectionTurns.ts +++ b/apps/server/src/persistence/Services/ProjectionTurns.ts @@ -158,4 +158,4 @@ export interface ProjectionTurnRepositoryShape { export class ProjectionTurnRepository extends ServiceMap.Service< ProjectionTurnRepository, ProjectionTurnRepositoryShape ->()("cut3/persistence/Services/ProjectionTurns/ProjectionTurnRepository") {} +>()("rowl/persistence/Services/ProjectionTurns/ProjectionTurnRepository") {} diff --git a/apps/server/src/persistence/Services/ProviderSessionRuntime.ts b/apps/server/src/persistence/Services/ProviderSessionRuntime.ts index 45f71a1deec..05cb451e6cc 100644 --- a/apps/server/src/persistence/Services/ProviderSessionRuntime.ts +++ b/apps/server/src/persistence/Services/ProviderSessionRuntime.ts @@ -88,4 +88,4 @@ export interface ProviderSessionRuntimeRepositoryShape { export class ProviderSessionRuntimeRepository extends ServiceMap.Service< ProviderSessionRuntimeRepository, ProviderSessionRuntimeRepositoryShape ->()("cut3/persistence/Services/ProviderSessionRuntime/ProviderSessionRuntimeRepository") {} +>()("rowl/persistence/Services/ProviderSessionRuntime/ProviderSessionRuntimeRepository") {} diff --git a/apps/server/src/piHarness.test.ts b/apps/server/src/piHarness.test.ts index 2a236aa4510..04c2c39ba33 100644 --- a/apps/server/src/piHarness.test.ts +++ b/apps/server/src/piHarness.test.ts @@ -8,7 +8,7 @@ import { createLockedPiSettingsManager } from "./piHarness.ts"; describe("createLockedPiSettingsManager", () => { it("preserves Pi runtime defaults while stripping package and resource discovery settings", () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-settings-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-settings-")); const agentDir = path.join(cwd, "agent"); const projectPiDir = path.join(cwd, ".pi"); fs.mkdirSync(agentDir, { recursive: true }); diff --git a/apps/server/src/piHarness.ts b/apps/server/src/piHarness.ts index 32b3613339c..343860192a6 100644 --- a/apps/server/src/piHarness.ts +++ b/apps/server/src/piHarness.ts @@ -21,16 +21,16 @@ export const PI_FULL_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "fin export const PI_PLAN_TOOL_NAMES = ["read", "bash", "grep", "find", "ls"] as const; export const PI_PROVIDER_SETUP_MESSAGE = - "CUT3 embeds Pi through the Pi Node SDK. Authenticate Pi outside CUT3 through the Pi CLI (`pi` or `bunx pi`) and `/login`, or populate ~/.pi/agent/auth.json / provider env vars. CUT3 intentionally disables Pi packages, extensions, prompt templates, skills, themes, AGENTS, and custom system-prompt discovery so CUT3 remains the only source of workspace instructions here."; + "Rowl embeds Pi through the Pi Node SDK. Authenticate Pi outside Rowl through the Pi CLI (`pi` or `bunx pi`) and `/login`, or populate ~/.pi/agent/auth.json / provider env vars. Rowl intentionally disables Pi packages, extensions, prompt templates, skills, themes, AGENTS, and custom system-prompt discovery so Rowl remains the only source of workspace instructions here."; export const PI_PLAN_MODE_PROMPT_PREFIX = ` -You are in CUT3 plan mode. +You are in Rowl plan mode. - Focus on exploration, clarification, and producing a detailed implementation plan. - Do not edit or write files in this mode. - You may inspect the repo and run non-mutating commands when they improve the plan. - If the user asks to implement immediately while still in plan mode, respond with a detailed plan instead of making repo-tracked changes. -- When you present the finalized plan, wrap it in ... so CUT3 can render it specially. +- When you present the finalized plan, wrap it in ... so Rowl can render it specially. `; export interface PiCatalogModelOption { diff --git a/apps/server/src/piSdkManager.test.ts b/apps/server/src/piSdkManager.test.ts index 72693b6568f..9574bb8ba06 100644 --- a/apps/server/src/piSdkManager.test.ts +++ b/apps/server/src/piSdkManager.test.ts @@ -147,7 +147,7 @@ async function flushMicrotasks() { describe("PiSdkManager", () => { it("emits turn.started before the background prompt completes", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-")); const deps = createSessionDependencies(cwd); const fakeSession = new FakeAgentSession(deps.model); let finishPrompt: (() => void) | undefined; @@ -218,7 +218,7 @@ describe("PiSdkManager", () => { }); it("applies Pi thinking levels and reports the effective options in session.configured", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-thinking-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-thinking-")); const deps = createSessionDependencies(cwd); const fakeSession = new FakeAgentSession(deps.model, [ "off", @@ -269,7 +269,7 @@ describe("PiSdkManager", () => { }); it("clamps unsupported Pi thinking levels during turn submission and re-emits config", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-thinking-turn-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-thinking-turn-")); const deps = createSessionDependencies(cwd); const fakeSession = new FakeAgentSession(deps.model, [ "off", @@ -334,7 +334,7 @@ describe("PiSdkManager", () => { }); it("opens and resolves CUT3 approvals around Pi tool execution", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-approval-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-approval-")); fs.writeFileSync(path.join(cwd, "README.md"), "# hello\n", "utf8"); const deps = createSessionDependencies(cwd); const fakeSession = new FakeAgentSession(deps.model); @@ -441,7 +441,7 @@ describe("PiSdkManager", () => { }); it("keeps the replacement Pi session registered after disposing the previous one", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-replace-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-replace-")); const deps = createSessionDependencies(cwd); const firstSession = new FakeAgentSession(deps.model); const secondSession = new FakeAgentSession(deps.model); @@ -500,7 +500,7 @@ describe("PiSdkManager", () => { }); it("blocks duplicate Pi session starts before createContext resolves", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-starting-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-starting-")); const deps = createSessionDependencies(cwd); const fakeSession = new FakeAgentSession(deps.model); @@ -552,7 +552,7 @@ describe("PiSdkManager", () => { }); it("ignores stale interrupt requests that target an older Pi turn id", async () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-pi-manager-interrupt-")); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-pi-manager-interrupt-")); const deps = createSessionDependencies(cwd); const fakeSession = new FakeAgentSession(deps.model); let finishPrompt: (() => void) | undefined; diff --git a/apps/server/src/piSdkManager.ts b/apps/server/src/piSdkManager.ts index 44adce3da87..87f2e71d756 100644 --- a/apps/server/src/piSdkManager.ts +++ b/apps/server/src/piSdkManager.ts @@ -1061,7 +1061,7 @@ export class PiSdkManager extends EventEmitter { this.emitSessionStarted(context); if (previousContext) { await this.disposeContext(previousContext, { - reason: "Pi session replaced by a newer CUT3 session.", + reason: "Pi session replaced by a newer Rowl session.", emitExit: false, }); } @@ -1210,7 +1210,7 @@ export class PiSdkManager extends EventEmitter { _requestId?: ApprovalRequestId, _answers?: unknown, ): Promise { - throw new Error("Pi user-input requests are not supported by this CUT3 integration."); + throw new Error("Pi user-input requests are not supported by this Rowl integration."); } async stopSession(threadId: ThreadId): Promise { diff --git a/apps/server/src/projectFaviconRoute.test.ts b/apps/server/src/projectFaviconRoute.test.ts index 15e68a82528..d7c79ef3df6 100644 --- a/apps/server/src/projectFaviconRoute.test.ts +++ b/apps/server/src/projectFaviconRoute.test.ts @@ -86,7 +86,7 @@ describe("tryHandleProjectFaviconRequest", () => { }); it("serves a well-known favicon file from the project root", async () => { - const projectDir = makeTempDir("cut3-favicon-route-root-"); + const projectDir = makeTempDir("rowl-favicon-route-root-"); fs.writeFileSync(path.join(projectDir, "favicon.svg"), "favicon", "utf8"); await withRouteServer(async (baseUrl) => { @@ -99,7 +99,7 @@ describe("tryHandleProjectFaviconRequest", () => { }); it("resolves icon href from source files when no well-known favicon exists", async () => { - const projectDir = makeTempDir("cut3-favicon-route-source-"); + const projectDir = makeTempDir("rowl-favicon-route-source-"); const iconPath = path.join(projectDir, "public", "brand", "logo.svg"); fs.mkdirSync(path.dirname(iconPath), { recursive: true }); fs.writeFileSync( @@ -118,7 +118,7 @@ describe("tryHandleProjectFaviconRequest", () => { }); it("resolves icon link when href appears before rel in HTML", async () => { - const projectDir = makeTempDir("cut3-favicon-route-html-order-"); + const projectDir = makeTempDir("rowl-favicon-route-html-order-"); const iconPath = path.join(projectDir, "public", "brand", "logo.svg"); fs.mkdirSync(path.dirname(iconPath), { recursive: true }); fs.writeFileSync( @@ -137,7 +137,7 @@ describe("tryHandleProjectFaviconRequest", () => { }); it("resolves object-style icon metadata when href appears before rel", async () => { - const projectDir = makeTempDir("cut3-favicon-route-obj-order-"); + const projectDir = makeTempDir("rowl-favicon-route-obj-order-"); const iconPath = path.join(projectDir, "public", "brand", "obj.svg"); fs.mkdirSync(path.dirname(iconPath), { recursive: true }); fs.mkdirSync(path.join(projectDir, "src"), { recursive: true }); @@ -158,7 +158,7 @@ describe("tryHandleProjectFaviconRequest", () => { }); it("serves a fallback favicon when no icon exists", async () => { - const projectDir = makeTempDir("cut3-favicon-route-fallback-"); + const projectDir = makeTempDir("rowl-favicon-route-fallback-"); await withRouteServer(async (baseUrl) => { const pathname = `/api/project-favicon?cwd=${encodeURIComponent(projectDir)}`; @@ -174,8 +174,8 @@ describe("tryHandleProjectFaviconRequest", () => { return; } - const projectDir = makeTempDir("cut3-favicon-route-symlink-root-"); - const externalDir = makeTempDir("cut3-favicon-route-external-root-"); + const projectDir = makeTempDir("rowl-favicon-route-symlink-root-"); + const externalDir = makeTempDir("rowl-favicon-route-external-root-"); const externalIconPath = path.join(externalDir, "outside.svg"); fs.writeFileSync(externalIconPath, "outside", "utf8"); fs.symlinkSync(externalIconPath, path.join(projectDir, "favicon.svg")); @@ -195,8 +195,8 @@ describe("tryHandleProjectFaviconRequest", () => { return; } - const projectDir = makeTempDir("cut3-favicon-route-symlink-href-"); - const externalDir = makeTempDir("cut3-favicon-route-external-href-"); + const projectDir = makeTempDir("rowl-favicon-route-symlink-href-"); + const externalDir = makeTempDir("rowl-favicon-route-external-href-"); const externalIconPath = path.join(externalDir, "outside.svg"); const linkedIconPath = path.join(projectDir, "public", "brand", "logo.svg"); fs.mkdirSync(path.dirname(linkedIconPath), { recursive: true }); diff --git a/apps/server/src/projectFaviconRoute.ts b/apps/server/src/projectFaviconRoute.ts index d706e854116..9bd1476f1ec 100644 --- a/apps/server/src/projectFaviconRoute.ts +++ b/apps/server/src/projectFaviconRoute.ts @@ -13,6 +13,11 @@ const FALLBACK_FAVICON_SVG = ` { if (readErr) { res.writeHead(500, { "Content-Type": "text/plain" }); res.end("Read error"); return; } + const dataStr = data.toString("utf8"); + if (dataStr.startsWith("data:image/") || dataStr.startsWith("data:text/plain")) { + const match = dataStr.match(/^data:([^;]+);base64,(.+)$/); + if (match) { + const mimeType = match[1]; + const base64Data = match[2]!; + const buffer = Buffer.from(base64Data, "base64"); + res.writeHead(200, { + "Content-Type": mimeType, + "Cache-Control": cacheControl, + }); + res.end(buffer); + return; + } + } res.writeHead(200, { "Content-Type": contentType, - "Cache-Control": "public, max-age=3600", + "Cache-Control": cacheControl, }); res.end(data); }); diff --git a/apps/server/src/projectWorkspaceMetadata.ts b/apps/server/src/projectWorkspaceMetadata.ts index e892546f106..754d5efaeb0 100644 --- a/apps/server/src/projectWorkspaceMetadata.ts +++ b/apps/server/src/projectWorkspaceMetadata.ts @@ -21,11 +21,11 @@ import { import { Schema } from "effect"; const AGENTS_FILE_NAME = "AGENTS.md"; -const COMMANDS_DIRECTORY_RELATIVE_PATH = ".cut3/commands"; -const SKILLS_DIRECTORY_RELATIVE_PATH = ".cut3/skills"; +const COMMANDS_DIRECTORY_RELATIVE_PATH = ".rowl/commands"; +const SKILLS_DIRECTORY_RELATIVE_PATH = ".rowl/skills"; const SKILL_FILE_NAME = "SKILL.md"; -const INIT_SECTION_START = ""; -const INIT_SECTION_END = ""; +const INIT_SECTION_START = ""; +const INIT_SECTION_END = ""; function safeReadTextFile(filePath: string): string | null { try { @@ -119,7 +119,7 @@ function buildInitManagedSection(cwd: string): string { const lines: string[] = [ INIT_SECTION_START, - "## CUT3 Init Snapshot", + "## Rowl Init Snapshot", "", `- Workspace root: ${path.basename(cwd)}`, ]; diff --git a/apps/server/src/provider/Layers/OpenCodeState.ts b/apps/server/src/provider/Layers/OpenCodeState.ts index c68ef0eafad..6ca90e1690d 100644 --- a/apps/server/src/provider/Layers/OpenCodeState.ts +++ b/apps/server/src/provider/Layers/OpenCodeState.ts @@ -300,6 +300,14 @@ export function parseOpenCodeAuthListOutput(output: string): ServerOpenCodeCrede return credentials; } +const OPENROUTER_PROVIDER_ID_NORMALIZATIONS: Record = { + zai: "z-ai", +}; + +function normalizeOpenRouterProviderId(providerId: string): string { + return OPENROUTER_PROVIDER_ID_NORMALIZATIONS[providerId] ?? providerId; +} + export function parseOpenCodeModelsOutput(output: string): ServerOpenCodeModel[] { const models: ServerOpenCodeModel[] = []; const seen = new Set(); @@ -315,12 +323,13 @@ export function parseOpenCodeModelsOutput(output: string): ServerOpenCodeModel[] continue; } - const providerId = match[1]?.trim(); + const rawProviderId = match[1]?.trim(); const modelId = match[2]?.trim(); - if (!providerId || !modelId) { + if (!rawProviderId || !modelId) { continue; } + const providerId = normalizeOpenRouterProviderId(rawProviderId); const slug = `${providerId}/${modelId}`; if (seen.has(slug)) { continue; diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 601339e79eb..39c049639cb 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -175,7 +175,7 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => { assert.strictEqual(status.authStatus, "unknown"); assert.strictEqual( status.message, - "Codex CLI v0.36.0 is too old for CUT3. Upgrade to v0.37.0 or newer and restart CUT3.", + "Codex CLI v0.36.0 is too old for Rowl. Upgrade to v0.37.0 or newer and restart Rowl.", ); }).pipe( Effect.provide( @@ -398,7 +398,7 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => { assert.strictEqual(status.authStatus, "unauthenticated"); assert.strictEqual( status.message, - "Pi is embedded in CUT3, but no authenticated Pi-backed models are currently available. Run `pi` (or `bunx pi`) and use `/login`, or populate ~/.pi/agent/auth.json / provider env vars.", + "Pi is embedded in Rowl, but no authenticated Pi-backed models are currently available. Run `pi` (or `bunx pi`) and use `/login`, or populate ~/.pi/agent/auth.json / provider env vars.", ); }), ); @@ -424,7 +424,7 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => { assert.strictEqual(typeof status.availableModels?.[0]?.supportsImageInput, "boolean"); assert.match( status.message ?? "", - /^Pi is ready with \d+ authenticated models?\. CUT3 reuses ~\/\.pi\/agent auth\/models config while keeping Pi resource discovery disabled by default\.$/, + /^Pi is ready with \d+ authenticated models?\. Rowl reuses ~\/\.pi\/agent auth\/models config while keeping Pi resource discovery disabled by default\.$/, ); }), ); diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts index cb052fc9a35..a4ca8be102c 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.ts @@ -700,7 +700,7 @@ export const checkPiProviderStatus: Effect.Effect = message: issueMessages.length > 0 ? `Pi is ready with ${snapshot.availableModels.length} authenticated model${snapshot.availableModels.length === 1 ? "" : "s"}, but local Pi config reported an issue: ${issueMessages[0]}.` - : `Pi is ready with ${snapshot.availableModels.length} authenticated model${snapshot.availableModels.length === 1 ? "" : "s"}. CUT3 reuses ~/.pi/agent auth/models config while keeping Pi resource discovery disabled by default.`, + : `Pi is ready with ${snapshot.availableModels.length} authenticated model${snapshot.availableModels.length === 1 ? "" : "s"}. Rowl reuses ~/.pi/agent auth/models config while keeping Pi resource discovery disabled by default.`, availableModels: snapshot.availableModels.map((model) => { const availableModel = { slug: model.slug, @@ -730,8 +730,8 @@ export const checkPiProviderStatus: Effect.Effect = checkedAt, message: issueMessages.length > 0 - ? `Pi is embedded in CUT3, but no authenticated Pi-backed models are currently available. Local Pi config also reported: ${issueMessages[0]}. Run \`pi\` (or \`bunx pi\`) and use \`/login\`, or populate ~/.pi/agent/auth.json / provider env vars.` - : "Pi is embedded in CUT3, but no authenticated Pi-backed models are currently available. Run `pi` (or `bunx pi`) and use `/login`, or populate ~/.pi/agent/auth.json / provider env vars.", + ? `Pi is embedded in Rowl, but no authenticated Pi-backed models are currently available. Local Pi config also reported: ${issueMessages[0]}. Run \`pi\` (or \`bunx pi\`) and use \`/login\`, or populate ~/.pi/agent/auth.json / provider env vars.` + : "Pi is embedded in Rowl, but no authenticated Pi-backed models are currently available. Run `pi` (or `bunx pi`) and use `/login`, or populate ~/.pi/agent/auth.json / provider env vars.", }; } catch (error) { return { diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts index c338672dc06..497c48cd426 100644 --- a/apps/server/src/provider/Layers/ProviderService.ts +++ b/apps/server/src/provider/Layers/ProviderService.ts @@ -246,7 +246,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => if (recoveredProviderOptions.missingSecrets.length > 0) { return yield* toValidationError( input.operation, - `Cannot recover thread '${input.binding.threadId}' because it depends on transient provider secrets (${recoveredProviderOptions.missingSecrets.join(", ")}) that were not persisted. Re-send a turn from CUT3 or export the required environment variables before restarting CUT3.`, + `Cannot recover thread '${input.binding.threadId}' because it depends on transient provider secrets (${recoveredProviderOptions.missingSecrets.join(", ")}) that were not persisted. Re-send a turn from Rowl or export the required environment variables before restarting Rowl.`, ); } diff --git a/apps/server/src/provider/Services/CodexAdapter.ts b/apps/server/src/provider/Services/CodexAdapter.ts index de7ebbd526f..25b275d160a 100644 --- a/apps/server/src/provider/Services/CodexAdapter.ts +++ b/apps/server/src/provider/Services/CodexAdapter.ts @@ -26,5 +26,5 @@ export interface CodexAdapterShape extends ProviderAdapterShape()( - "cut3/provider/Services/CodexAdapter", + "rowl/provider/Services/CodexAdapter", ) {} diff --git a/apps/server/src/provider/Services/CopilotAdapter.ts b/apps/server/src/provider/Services/CopilotAdapter.ts index 2dbadc932f8..c4191a3e09c 100644 --- a/apps/server/src/provider/Services/CopilotAdapter.ts +++ b/apps/server/src/provider/Services/CopilotAdapter.ts @@ -8,5 +8,5 @@ export interface CopilotAdapterShape extends ProviderAdapterShape()( - "cut3/provider/Services/CopilotAdapter", + "rowl/provider/Services/CopilotAdapter", ) {} diff --git a/apps/server/src/provider/Services/KimiAdapter.ts b/apps/server/src/provider/Services/KimiAdapter.ts index e924aeddf6e..8552f564430 100644 --- a/apps/server/src/provider/Services/KimiAdapter.ts +++ b/apps/server/src/provider/Services/KimiAdapter.ts @@ -8,5 +8,5 @@ export interface KimiAdapterShape extends ProviderAdapterShape()( - "cut3/provider/Services/KimiAdapter", + "rowl/provider/Services/KimiAdapter", ) {} diff --git a/apps/server/src/provider/Services/OpenCodeAdapter.ts b/apps/server/src/provider/Services/OpenCodeAdapter.ts index 4acfee52ac8..09384ecfc8d 100644 --- a/apps/server/src/provider/Services/OpenCodeAdapter.ts +++ b/apps/server/src/provider/Services/OpenCodeAdapter.ts @@ -8,5 +8,5 @@ export interface OpenCodeAdapterShape extends ProviderAdapterShape()( - "cut3/provider/Services/OpenCodeAdapter", + "rowl/provider/Services/OpenCodeAdapter", ) {} diff --git a/apps/server/src/provider/Services/OpenCodeState.ts b/apps/server/src/provider/Services/OpenCodeState.ts index 594fb1ea9ba..cd493257e96 100644 --- a/apps/server/src/provider/Services/OpenCodeState.ts +++ b/apps/server/src/provider/Services/OpenCodeState.ts @@ -19,5 +19,5 @@ export interface OpenCodeStateShape { } export class OpenCodeState extends ServiceMap.Service()( - "cut3/provider/Services/OpenCodeState", + "rowl/provider/Services/OpenCodeState", ) {} diff --git a/apps/server/src/provider/Services/PiAdapter.ts b/apps/server/src/provider/Services/PiAdapter.ts index 679a71fcecb..be52d312830 100644 --- a/apps/server/src/provider/Services/PiAdapter.ts +++ b/apps/server/src/provider/Services/PiAdapter.ts @@ -8,5 +8,5 @@ export interface PiAdapterShape extends ProviderAdapterShape()( - "cut3/provider/Services/PiAdapter", + "rowl/provider/Services/PiAdapter", ) {} diff --git a/apps/server/src/provider/Services/ProviderAdapterRegistry.ts b/apps/server/src/provider/Services/ProviderAdapterRegistry.ts index 4c6c4c7b4af..aa48342e75e 100644 --- a/apps/server/src/provider/Services/ProviderAdapterRegistry.ts +++ b/apps/server/src/provider/Services/ProviderAdapterRegistry.ts @@ -37,6 +37,6 @@ export interface ProviderAdapterRegistryShape { export class ProviderAdapterRegistry extends ServiceMap.Service< ProviderAdapterRegistry, ProviderAdapterRegistryShape ->()("cut3/provider/Services/ProviderAdapterRegistry") {} +>()("rowl/provider/Services/ProviderAdapterRegistry") {} // Dummy comment for workflow testing. diff --git a/apps/server/src/provider/Services/ProviderHealth.ts b/apps/server/src/provider/Services/ProviderHealth.ts index cd09d8ddbc4..4c102afac6d 100644 --- a/apps/server/src/provider/Services/ProviderHealth.ts +++ b/apps/server/src/provider/Services/ProviderHealth.ts @@ -18,5 +18,5 @@ export interface ProviderHealthShape { } export class ProviderHealth extends ServiceMap.Service()( - "cut3/provider/Services/ProviderHealth", + "rowl/provider/Services/ProviderHealth", ) {} diff --git a/apps/server/src/provider/Services/ProviderService.ts b/apps/server/src/provider/Services/ProviderService.ts index f4aaac864d4..db2f46268fb 100644 --- a/apps/server/src/provider/Services/ProviderService.ts +++ b/apps/server/src/provider/Services/ProviderService.ts @@ -111,5 +111,5 @@ export interface ProviderServiceShape { * ProviderService - Service tag for provider orchestration. */ export class ProviderService extends ServiceMap.Service()( - "cut3/provider/Services/ProviderService", + "rowl/provider/Services/ProviderService", ) {} diff --git a/apps/server/src/provider/Services/ProviderSessionDirectory.ts b/apps/server/src/provider/Services/ProviderSessionDirectory.ts index faf08b43524..d687425928c 100644 --- a/apps/server/src/provider/Services/ProviderSessionDirectory.ts +++ b/apps/server/src/provider/Services/ProviderSessionDirectory.ts @@ -59,4 +59,4 @@ export interface ProviderSessionDirectoryShape { export class ProviderSessionDirectory extends ServiceMap.Service< ProviderSessionDirectory, ProviderSessionDirectoryShape ->()("cut3/provider/Services/ProviderSessionDirectory") {} +>()("rowl/provider/Services/ProviderSessionDirectory") {} diff --git a/apps/server/src/provider/acpRuntimeShared.ts b/apps/server/src/provider/acpRuntimeShared.ts index eef60c7f2a3..6237bb222f1 100644 --- a/apps/server/src/provider/acpRuntimeShared.ts +++ b/apps/server/src/provider/acpRuntimeShared.ts @@ -201,14 +201,31 @@ export function permissionDecisionFromOutcome( } } -export function killChildTree(child: ChildProcessWithoutNullStreams): void { +const ENABLE_PROVIDER_EVENT_LOGS = process.env.ROWL_ENABLE_PROVIDER_EVENT_LOGS === "1"; + +function providerLog(label: string, ...args: unknown[]): void { + if (ENABLE_PROVIDER_EVENT_LOGS) { + console.log(`[Rowl:${label}]`, ...args); + } +} + +export { providerLog }; + +export function killChildTree( + child: ChildProcessWithoutNullStreams, + signal: NodeJS.Signals = "SIGTERM", +): void { if (process.platform === "win32" && child.pid !== undefined) { try { spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); + providerLog("Process", `Terminated child ${child.pid} via taskkill`); return; } catch { // Fallback to direct kill below. } } - child.kill(); + if (!child.killed) { + child.kill(signal); + providerLog("Process", `Sent ${signal} to child ${child.pid}`); + } } diff --git a/apps/server/src/provider/codexCliVersion.ts b/apps/server/src/provider/codexCliVersion.ts index 9f7a3c22abb..67c2cc5f572 100644 --- a/apps/server/src/provider/codexCliVersion.ts +++ b/apps/server/src/provider/codexCliVersion.ts @@ -137,5 +137,5 @@ export function isCodexCliVersionSupported(version: string): boolean { export function formatCodexCliUpgradeMessage(version: string | null): string { const versionLabel = version ? `v${version}` : "the installed version"; - return `Codex CLI ${versionLabel} is too old for CUT3. Upgrade to v${MINIMUM_CODEX_CLI_VERSION} or newer and restart CUT3.`; + return `Codex CLI ${versionLabel} is too old for Rowl. Upgrade to v${MINIMUM_CODEX_CLI_VERSION} or newer and restart Rowl.`; } diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts index 3e142f2a181..026147cedf0 100644 --- a/apps/server/src/serverLayers.ts +++ b/apps/server/src/serverLayers.ts @@ -42,7 +42,7 @@ import { BunPtyAdapterLive } from "./terminal/Layers/BunPTY"; import { NodePtyAdapterLive } from "./terminal/Layers/NodePTY"; import { AnalyticsService } from "./telemetry/Services/AnalyticsService"; -const ENABLE_PROVIDER_EVENT_LOGS_ENV = "CUT3_ENABLE_PROVIDER_EVENT_LOGS"; +const ENABLE_PROVIDER_EVENT_LOGS_ENV = "ROWL_ENABLE_PROVIDER_EVENT_LOGS"; function shouldEnableProviderEventLogs(): boolean { const raw = process.env[ENABLE_PROVIDER_EVENT_LOGS_ENV]?.trim().toLowerCase(); diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.test.ts b/apps/server/src/telemetry/Layers/AnalyticsService.test.ts index d42f7bec972..02d079eaf5d 100644 --- a/apps/server/src/telemetry/Layers/AnalyticsService.test.ts +++ b/apps/server/src/telemetry/Layers/AnalyticsService.test.ts @@ -48,10 +48,10 @@ it.layer(NodeServices.layer)("AnalyticsService test", (it) => { const telemetryLayer = AnalyticsServiceLayerLive.pipe(Layer.provideMerge(serverConfigLayer)); const configLayer = ConfigProvider.layer( ConfigProvider.fromUnknown({ - CUT3_TELEMETRY_ENABLED: true, - CUT3_POSTHOG_KEY: "phc_test_key", - CUT3_POSTHOG_HOST: "", - CUT3_TELEMETRY_FLUSH_BATCH_SIZE: 20, + ROWL_TELEMETRY_ENABLED: true, + ROWL_POSTHOG_KEY: "phc_test_key", + ROWL_POSTHOG_HOST: "", + ROWL_TELEMETRY_FLUSH_BATCH_SIZE: 20, }), ); const batchServerLayer = HttpServer.serve( diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.ts b/apps/server/src/telemetry/Layers/AnalyticsService.ts index 74939ccfd8a..c8b3150b295 100644 --- a/apps/server/src/telemetry/Layers/AnalyticsService.ts +++ b/apps/server/src/telemetry/Layers/AnalyticsService.ts @@ -22,15 +22,15 @@ interface BufferedAnalyticsEvent { } const TelemetryEnvConfig = Config.all({ - posthogKey: Config.string("CUT3_POSTHOG_KEY").pipe( + posthogKey: Config.string("ROWL_POSTHOG_KEY").pipe( Config.withDefault("phc_XOWci4oZP4VvLiEyrFqkFjP4CZn55mjYYBMREK5Wd6m"), ), - posthogHost: Config.string("CUT3_POSTHOG_HOST").pipe( + posthogHost: Config.string("ROWL_POSTHOG_HOST").pipe( Config.withDefault("https://us.i.posthog.com"), ), - enabled: Config.boolean("CUT3_TELEMETRY_ENABLED").pipe(Config.withDefault(true)), - flushBatchSize: Config.number("CUT3_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)), - maxBufferedEvents: Config.number("CUT3_TELEMETRY_MAX_BUFFERED_EVENTS").pipe( + enabled: Config.boolean("ROWL_TELEMETRY_ENABLED").pipe(Config.withDefault(true)), + flushBatchSize: Config.number("ROWL_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)), + maxBufferedEvents: Config.number("ROWL_TELEMETRY_MAX_BUFFERED_EVENTS").pipe( Config.withDefault(1_000), ), }); diff --git a/apps/server/src/telemetry/Services/AnalyticsService.ts b/apps/server/src/telemetry/Services/AnalyticsService.ts index 1ac1f26c620..58cd150756b 100644 --- a/apps/server/src/telemetry/Services/AnalyticsService.ts +++ b/apps/server/src/telemetry/Services/AnalyticsService.ts @@ -24,7 +24,7 @@ export interface AnalyticsServiceShape { } export class AnalyticsService extends ServiceMap.Service()( - "cut3/telemetry/Services/AnalyticsService", + "rowl/telemetry/Services/AnalyticsService", ) { static readonly layerTest = Layer.succeed(AnalyticsService, { record: () => Effect.void, diff --git a/apps/server/src/terminal/Layers/Manager.test.ts b/apps/server/src/terminal/Layers/Manager.test.ts index ccdec95c568..1ac0410467d 100644 --- a/apps/server/src/terminal/Layers/Manager.test.ts +++ b/apps/server/src/terminal/Layers/Manager.test.ts @@ -189,7 +189,7 @@ describe("TerminalManager", () => { ptyAdapter?: FakePtyAdapter; } = {}, ) { - const logsDir = fs.mkdtempSync(path.join(os.tmpdir(), "cut3-terminal-")); + const logsDir = fs.mkdtempSync(path.join(os.tmpdir(), "rowl-terminal-")); tempDirs.push(logsDir); const ptyAdapter = options.ptyAdapter ?? new FakePtyAdapter(); const manager = new TerminalManagerRuntime({ @@ -721,7 +721,7 @@ describe("TerminalManager", () => { }; setEnv("PORT", "5173"); - setEnv("CUT3_PORT", "3773"); + setEnv("ROWL_PORT", "3773"); setEnv("VITE_DEV_SERVER_URL", "http://localhost:5173"); setEnv("TEST_TERMINAL_KEEP", "keep-me"); @@ -733,7 +733,7 @@ describe("TerminalManager", () => { if (!spawnInput) return; expect(spawnInput.env.PORT).toBeUndefined(); - expect(spawnInput.env.CUT3_PORT).toBeUndefined(); + expect(spawnInput.env.ROWL_PORT).toBeUndefined(); expect(spawnInput.env.VITE_DEV_SERVER_URL).toBeUndefined(); expect(spawnInput.env.TEST_TERMINAL_KEEP).toBe("keep-me"); @@ -748,8 +748,8 @@ describe("TerminalManager", () => { await manager.open( openInput({ env: { - CUT3_PROJECT_ROOT: "/repo", - CUT3_WORKTREE_PATH: "/repo/worktree-a", + ROWL_PROJECT_ROOT: "/repo", + ROWL_WORKTREE_PATH: "/repo/worktree-a", CUSTOM_FLAG: "1", }, }), @@ -758,8 +758,8 @@ describe("TerminalManager", () => { expect(spawnInput).toBeDefined(); if (!spawnInput) return; - expect(spawnInput.env.CUT3_PROJECT_ROOT).toBe("/repo"); - expect(spawnInput.env.CUT3_WORKTREE_PATH).toBe("/repo/worktree-a"); + expect(spawnInput.env.ROWL_PROJECT_ROOT).toBe("/repo"); + expect(spawnInput.env.ROWL_WORKTREE_PATH).toBe("/repo/worktree-a"); expect(spawnInput.env.CUSTOM_FLAG).toBe("1"); await manager.dispose(); diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index 23caf2441ce..44d6df5a98d 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -274,7 +274,7 @@ function toSessionKey(threadId: string, terminalId: string): string { function shouldExcludeTerminalEnvKey(key: string): boolean { const normalizedKey = key.toUpperCase(); - if (normalizedKey.startsWith("CUT3_")) { + if (normalizedKey.startsWith("ROWL_")) { return true; } if (normalizedKey.startsWith("VITE_")) { diff --git a/apps/server/src/terminal/Services/Manager.ts b/apps/server/src/terminal/Services/Manager.ts index 5e9c82213fc..dbf8d8ada35 100644 --- a/apps/server/src/terminal/Services/Manager.ts +++ b/apps/server/src/terminal/Services/Manager.ts @@ -116,5 +116,5 @@ export interface TerminalManagerShape { * TerminalManager - Service tag for terminal session orchestration. */ export class TerminalManager extends ServiceMap.Service()( - "cut3/terminal/Services/Manager/TerminalManager", + "rowl/terminal/Services/Manager/TerminalManager", ) {} diff --git a/apps/server/src/terminal/Services/PTY.ts b/apps/server/src/terminal/Services/PTY.ts index d47401a695b..64c23eac5a1 100644 --- a/apps/server/src/terminal/Services/PTY.ts +++ b/apps/server/src/terminal/Services/PTY.ts @@ -54,5 +54,5 @@ export interface PtyAdapterShape { * PtyAdapter - Service tag for PTY process integration. */ export class PtyAdapter extends ServiceMap.Service()( - "cut3/terminal/Services/PTY/PtyAdapter", + "rowl/terminal/Services/PTY/PtyAdapter", ) {} diff --git a/apps/server/src/threadArtifacts.ts b/apps/server/src/threadArtifacts.ts index b0e42337c22..92631db709c 100644 --- a/apps/server/src/threadArtifacts.ts +++ b/apps/server/src/threadArtifacts.ts @@ -273,6 +273,7 @@ export function buildThreadContinuationSummaryFromState( id: ThreadId.makeUnsafe("restored-thread"), projectId: ProjectId.makeUnsafe("restored-project"), title: state.title, + goal: null, model: state.model, runtimeMode: state.runtimeMode, interactionMode: state.interactionMode, diff --git a/apps/server/src/workspaceEntries.test.ts b/apps/server/src/workspaceEntries.test.ts index 4b5275cd142..58a4996f508 100644 --- a/apps/server/src/workspaceEntries.test.ts +++ b/apps/server/src/workspaceEntries.test.ts @@ -38,7 +38,7 @@ describe("searchWorkspaceEntries", () => { }); it("returns files and directories relative to cwd", async () => { - const cwd = makeTempDir("cut3-workspace-entries-"); + const cwd = makeTempDir("rowl-workspace-entries-"); writeFile(cwd, "src/components/Composer.tsx"); writeFile(cwd, "src/index.ts"); writeFile(cwd, "README.md"); @@ -58,7 +58,7 @@ describe("searchWorkspaceEntries", () => { }); it("filters and ranks entries by query", async () => { - const cwd = makeTempDir("cut3-workspace-query-"); + const cwd = makeTempDir("rowl-workspace-query-"); writeFile(cwd, "src/components/Composer.tsx"); writeFile(cwd, "src/components/composePrompt.ts"); writeFile(cwd, "docs/composition.md"); @@ -71,7 +71,7 @@ describe("searchWorkspaceEntries", () => { }); it("supports fuzzy subsequence queries for composer path search", async () => { - const cwd = makeTempDir("cut3-workspace-fuzzy-query-"); + const cwd = makeTempDir("rowl-workspace-fuzzy-query-"); writeFile(cwd, "src/components/Composer.tsx"); writeFile(cwd, "src/components/composePrompt.ts"); writeFile(cwd, "docs/composition.md"); @@ -85,7 +85,7 @@ describe("searchWorkspaceEntries", () => { }); it("tracks truncation without sorting every fuzzy match", async () => { - const cwd = makeTempDir("cut3-workspace-fuzzy-limit-"); + const cwd = makeTempDir("rowl-workspace-fuzzy-limit-"); writeFile(cwd, "src/components/Composer.tsx"); writeFile(cwd, "src/components/composePrompt.ts"); writeFile(cwd, "docs/composition.md"); @@ -97,7 +97,7 @@ describe("searchWorkspaceEntries", () => { }); it("excludes gitignored paths for git repositories", async () => { - const cwd = makeTempDir("cut3-workspace-gitignore-"); + const cwd = makeTempDir("rowl-workspace-gitignore-"); runGit(cwd, ["init"]); writeFile(cwd, ".gitignore", ".convex/\nconvex/\nignored.txt\n"); writeFile(cwd, "src/keep.ts", "export {};"); @@ -116,7 +116,7 @@ describe("searchWorkspaceEntries", () => { }); it("excludes tracked paths that match ignore rules", async () => { - const cwd = makeTempDir("cut3-workspace-tracked-gitignore-"); + const cwd = makeTempDir("rowl-workspace-tracked-gitignore-"); runGit(cwd, ["init"]); writeFile(cwd, ".convex/local-storage/data.json", "{}"); writeFile(cwd, "src/keep.ts", "export {};"); @@ -132,7 +132,7 @@ describe("searchWorkspaceEntries", () => { }); it("excludes .convex in non-git workspaces", async () => { - const cwd = makeTempDir("cut3-workspace-non-git-convex-"); + const cwd = makeTempDir("rowl-workspace-non-git-convex-"); writeFile(cwd, ".convex/local-storage/data.json", "{}"); writeFile(cwd, "src/keep.ts", "export {};"); @@ -145,7 +145,7 @@ describe("searchWorkspaceEntries", () => { }); it("deduplicates concurrent index builds for the same cwd", async () => { - const cwd = makeTempDir("cut3-workspace-concurrent-build-"); + const cwd = makeTempDir("rowl-workspace-concurrent-build-"); writeFile(cwd, "src/components/Composer.tsx"); let rootReadCount = 0; @@ -170,7 +170,7 @@ describe("searchWorkspaceEntries", () => { }); it("limits concurrent directory reads while walking the filesystem", async () => { - const cwd = makeTempDir("cut3-workspace-read-concurrency-"); + const cwd = makeTempDir("rowl-workspace-read-concurrency-"); for (let index = 0; index < 80; index += 1) { writeFile(cwd, `group-${index}/entry-${index}.ts`, "export {};"); } diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index ef71ccf560b..31d5f2cb95c 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -528,7 +528,7 @@ describe("WebSocket Server", () => { throw new Error("Test server is already running"); } - const stateDir = options.stateDir ?? makeTempDir("cut3-ws-state-"); + const stateDir = options.stateDir ?? makeTempDir("rowl-ws-state-"); const scope = await Effect.runPromise(Scope.make("sequential")); const persistenceLayer = options.persistenceLayer ?? SqlitePersistenceMemory; const providerLayer = options.providerLayer ?? makeServerProviderLayer(); @@ -632,7 +632,7 @@ describe("WebSocket Server", () => { }); it("serves persisted attachments from stateDir", async () => { - const stateDir = makeTempDir("cut3-state-attachments-"); + const stateDir = makeTempDir("rowl-state-attachments-"); const attachmentPath = path.join(stateDir, "attachments", "thread-a", "message-a", "0.png"); fs.mkdirSync(path.dirname(attachmentPath), { recursive: true }); fs.writeFileSync(attachmentPath, Buffer.from("hello-attachment")); @@ -650,7 +650,7 @@ describe("WebSocket Server", () => { }); it("serves persisted attachments for URL-encoded paths", async () => { - const stateDir = makeTempDir("cut3-state-attachments-encoded-"); + const stateDir = makeTempDir("rowl-state-attachments-encoded-"); const attachmentPath = path.join( stateDir, "attachments", @@ -676,7 +676,7 @@ describe("WebSocket Server", () => { }); it("requires the auth token for attachment routes when auth is enabled", async () => { - const stateDir = makeTempDir("cut3-state-attachments-auth-"); + const stateDir = makeTempDir("rowl-state-attachments-auth-"); const attachmentPath = path.join(stateDir, "attachments", "thread-a", "message-a", "0.png"); fs.mkdirSync(path.dirname(attachmentPath), { recursive: true }); fs.writeFileSync(attachmentPath, Buffer.from("hello-attachment")); @@ -703,7 +703,7 @@ describe("WebSocket Server", () => { }); it("rejects attachment routes from unexpected browser origins in unauthenticated web mode", async () => { - const stateDir = makeTempDir("cut3-state-attachments-origin-"); + const stateDir = makeTempDir("rowl-state-attachments-origin-"); const attachmentPath = path.join(stateDir, "attachments", "thread-a", "message-a", "0.png"); fs.mkdirSync(path.dirname(attachmentPath), { recursive: true }); fs.writeFileSync(attachmentPath, Buffer.from("hello-attachment")); @@ -725,8 +725,8 @@ describe("WebSocket Server", () => { }); it("requires auth for project favicon requests and limits them to authorized workspaces", async () => { - const workspace = makeTempDir("cut3-project-favicon-auth-"); - const outside = makeTempDir("cut3-project-favicon-auth-outside-"); + const workspace = makeTempDir("rowl-project-favicon-auth-"); + const outside = makeTempDir("rowl-project-favicon-auth-outside-"); fs.writeFileSync(path.join(workspace, "favicon.svg"), "workspace", "utf8"); fs.writeFileSync(path.join(outside, "favicon.svg"), "outside", "utf8"); @@ -760,7 +760,7 @@ describe("WebSocket Server", () => { }); it("rejects project favicon requests from unexpected browser origins in unauthenticated web mode", async () => { - const workspace = makeTempDir("cut3-project-favicon-origin-"); + const workspace = makeTempDir("rowl-project-favicon-origin-"); fs.writeFileSync(path.join(workspace, "favicon.svg"), "workspace", "utf8"); server = await createTestServer({ @@ -783,8 +783,8 @@ describe("WebSocket Server", () => { }); it("serves static index for root path", async () => { - const stateDir = makeTempDir("cut3-state-static-root-"); - const staticDir = makeTempDir("cut3-static-root-"); + const stateDir = makeTempDir("rowl-state-static-root-"); + const staticDir = makeTempDir("rowl-static-root-"); fs.writeFileSync(path.join(staticDir, "index.html"), "

static-root

", "utf8"); server = await createTestServer({ cwd: "/test/project", stateDir, staticDir }); @@ -798,8 +798,8 @@ describe("WebSocket Server", () => { }); it("rejects static path traversal attempts", async () => { - const stateDir = makeTempDir("cut3-state-static-traversal-"); - const staticDir = makeTempDir("cut3-static-traversal-"); + const stateDir = makeTempDir("rowl-state-static-traversal-"); + const staticDir = makeTempDir("rowl-static-traversal-"); fs.writeFileSync(path.join(staticDir, "index.html"), "

safe

", "utf8"); server = await createTestServer({ cwd: "/test/project", stateDir, staticDir }); @@ -899,7 +899,7 @@ describe("WebSocket Server", () => { }); it("includes bootstrap ids in welcome when cwd project and thread already exist", async () => { - const stateDir = makeTempDir("cut3-state-bootstrap-existing-"); + const stateDir = makeTempDir("rowl-state-bootstrap-existing-"); const persistenceLayer = makeSqlitePersistenceLive(path.join(stateDir, "state.sqlite")).pipe( Layer.provide(NodeServices.layer), ); @@ -979,7 +979,7 @@ describe("WebSocket Server", () => { }); it("responds to server.getConfig", async () => { - const stateDir = makeTempDir("cut3-state-get-config-"); + const stateDir = makeTempDir("rowl-state-get-config-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "[]", "utf8"); @@ -1005,7 +1005,7 @@ describe("WebSocket Server", () => { }); it("reruns provider health checks for each server.getConfig request", async () => { - const stateDir = makeTempDir("cut3-state-get-config-provider-refresh-"); + const stateDir = makeTempDir("rowl-state-get-config-provider-refresh-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "[]", "utf8"); @@ -1074,7 +1074,7 @@ describe("WebSocket Server", () => { }); it("reports MCP support by provider in server.getConfig", async () => { - const stateDir = makeTempDir("cut3-state-get-config-mcp-support-"); + const stateDir = makeTempDir("rowl-state-get-config-mcp-support-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "[]", "utf8"); @@ -1119,7 +1119,7 @@ describe("WebSocket Server", () => { }); it("surfaces inspected OpenCode MCP servers in server.getConfig", async () => { - const stateDir = makeTempDir("cut3-state-get-config-opencode-mcp-"); + const stateDir = makeTempDir("rowl-state-get-config-opencode-mcp-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "[]", "utf8"); @@ -1189,7 +1189,7 @@ describe("WebSocket Server", () => { }); it("refreshes cached OpenCode MCP servers after runtime inspection", async () => { - const stateDir = makeTempDir("cut3-state-get-config-opencode-mcp-refresh-"); + const stateDir = makeTempDir("rowl-state-get-config-opencode-mcp-refresh-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "[]", "utf8"); @@ -1283,7 +1283,7 @@ describe("WebSocket Server", () => { }, 15_000); it("bootstraps default keybindings file when missing", async () => { - const stateDir = makeTempDir("cut3-state-bootstrap-keybindings-"); + const stateDir = makeTempDir("rowl-state-bootstrap-keybindings-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); expect(fs.existsSync(keybindingsPath)).toBe(false); @@ -1314,7 +1314,7 @@ describe("WebSocket Server", () => { }); it("falls back to defaults and reports malformed keybindings config issues", async () => { - const stateDir = makeTempDir("cut3-state-malformed-keybindings-"); + const stateDir = makeTempDir("rowl-state-malformed-keybindings-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "{ not-json", "utf8"); @@ -1346,7 +1346,7 @@ describe("WebSocket Server", () => { }); it("ignores invalid keybinding entries but keeps valid entries and reports issues", async () => { - const stateDir = makeTempDir("cut3-state-partial-invalid-keybindings-"); + const stateDir = makeTempDir("rowl-state-partial-invalid-keybindings-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync( keybindingsPath, @@ -1397,7 +1397,7 @@ describe("WebSocket Server", () => { }); it("pushes server.configUpdated issues when keybindings file changes", async () => { - const stateDir = makeTempDir("cut3-state-keybindings-watch-"); + const stateDir = makeTempDir("rowl-state-keybindings-watch-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync(keybindingsPath, "[]", "utf8"); @@ -1457,7 +1457,7 @@ describe("WebSocket Server", () => { }); it("allows shell.openInEditor for the keybindings config path", async () => { - const stateDir = makeTempDir("cut3-state-open-keybindings-"); + const stateDir = makeTempDir("rowl-state-open-keybindings-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); const openCalls: Array<{ cwd: string; editor: string }> = []; const openService: OpenShape = { @@ -1489,7 +1489,7 @@ describe("WebSocket Server", () => { }); it("reads keybindings from the configured state directory", async () => { - const stateDir = makeTempDir("cut3-state-keybindings-"); + const stateDir = makeTempDir("rowl-state-keybindings-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync( keybindingsPath, @@ -1525,7 +1525,7 @@ describe("WebSocket Server", () => { }); it("upserts keybinding rules and updates cached server config", async () => { - const stateDir = makeTempDir("cut3-state-upsert-keybinding-"); + const stateDir = makeTempDir("rowl-state-upsert-keybinding-"); const keybindingsPath = path.join(stateDir, "keybindings.json"); fs.writeFileSync( keybindingsPath, @@ -1647,7 +1647,7 @@ describe("WebSocket Server", () => { const [ws] = await connectAndAwaitWelcome(port); connections.push(ws); - const workspaceRoot = makeTempDir("cut3-ws-diff-project-"); + const workspaceRoot = makeTempDir("rowl-ws-diff-project-"); const createdAt = new Date().toISOString(); const createProjectResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.dispatchCommand, { type: "project.create", @@ -1725,7 +1725,7 @@ describe("WebSocket Server", () => { const [ws] = await connectAndAwaitWelcome(port); connections.push(ws); - const workspaceRoot = makeTempDir("cut3-ws-project-"); + const workspaceRoot = makeTempDir("rowl-ws-project-"); const createdAt = new Date().toISOString(); const createProjectResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.dispatchCommand, { type: "project.create", @@ -1847,7 +1847,7 @@ describe("WebSocket Server", () => { const [ws] = await connectAndAwaitWelcome(port); connections.push(ws); - const workspaceRoot = makeTempDir("cut3-ws-openrouter-project-"); + const workspaceRoot = makeTempDir("rowl-ws-openrouter-project-"); const createdAt = new Date().toISOString(); const createProjectResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.dispatchCommand, { type: "project.create", @@ -1965,7 +1965,7 @@ describe("WebSocket Server", () => { const [ws] = await connectAndAwaitWelcome(port); connections.push(ws); - const workspaceRoot = makeTempDir("cut3-ws-opencode-project-"); + const workspaceRoot = makeTempDir("rowl-ws-opencode-project-"); const createdAt = new Date().toISOString(); const createProjectResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.dispatchCommand, { type: "project.create", @@ -2039,7 +2039,7 @@ describe("WebSocket Server", () => { }); it("routes terminal RPC methods and broadcasts terminal events", async () => { - const cwd = makeTempDir("cut3-ws-terminal-cwd-"); + const cwd = makeTempDir("rowl-ws-terminal-cwd-"); const terminalManager = new MockTerminalManager(); server = await createTestServer({ cwd, @@ -2187,7 +2187,7 @@ describe("WebSocket Server", () => { }; try { - const workspace = makeTempDir("cut3-ws-handler-still-usable-"); + const workspace = makeTempDir("rowl-ws-handler-still-usable-"); fs.writeFileSync(path.join(workspace, "file.txt"), "ok\n", "utf8"); server = await createTestServer({ cwd: workspace, open: brokenOpenService }); @@ -2257,7 +2257,7 @@ describe("WebSocket Server", () => { }); it("supports projects.searchEntries", async () => { - const workspace = makeTempDir("cut3-ws-workspace-entries-"); + const workspace = makeTempDir("rowl-ws-workspace-entries-"); fs.mkdirSync(path.join(workspace, "src", "components"), { recursive: true }); fs.writeFileSync( path.join(workspace, "src", "components", "Composer.tsx"), @@ -2291,8 +2291,8 @@ describe("WebSocket Server", () => { }); it("rejects project search requests that escape through symlinked directories", async () => { - const workspace = makeTempDir("cut3-ws-symlink-search-"); - const outside = makeTempDir("cut3-ws-symlink-search-outside-"); + const workspace = makeTempDir("rowl-ws-symlink-search-"); + const outside = makeTempDir("rowl-ws-symlink-search-outside-"); const linkPath = path.join(workspace, "outside-link"); createDirectorySymlink(outside, linkPath); @@ -2314,7 +2314,7 @@ describe("WebSocket Server", () => { }); it("supports projects.writeFile within the workspace root", async () => { - const workspace = makeTempDir("cut3-ws-write-file-"); + const workspace = makeTempDir("rowl-ws-write-file-"); server = await createTestServer({ cwd: workspace }); const addr = server.address(); @@ -2339,7 +2339,7 @@ describe("WebSocket Server", () => { }); it("rejects projects.writeFile paths outside the workspace root", async () => { - const workspace = makeTempDir("cut3-ws-write-file-reject-"); + const workspace = makeTempDir("rowl-ws-write-file-reject-"); server = await createTestServer({ cwd: workspace }); const addr = server.address(); @@ -2362,8 +2362,8 @@ describe("WebSocket Server", () => { }); it("rejects projects.writeFile paths that escape through symlinked directories", async () => { - const workspace = makeTempDir("cut3-ws-write-file-symlink-"); - const outside = makeTempDir("cut3-ws-write-file-symlink-outside-"); + const workspace = makeTempDir("rowl-ws-write-file-symlink-"); + const outside = makeTempDir("rowl-ws-write-file-symlink-outside-"); const linkPath = path.join(workspace, "outside-link"); createDirectorySymlink(outside, linkPath); @@ -2388,7 +2388,7 @@ describe("WebSocket Server", () => { }); it("rejects project file writes outside authorized workspaces", async () => { - const workspace = makeTempDir("cut3-ws-write-file-authorized-"); + const workspace = makeTempDir("rowl-ws-write-file-authorized-"); server = await createTestServer({ cwd: workspace }); const addr = server.address(); @@ -2581,7 +2581,7 @@ describe("WebSocket Server", () => { }); it("rejects git pull request routes outside authorized workspaces", async () => { - const workspace = makeTempDir("cut3-git-pr-authz-"); + const workspace = makeTempDir("rowl-git-pr-authz-"); const unauthorizedCwd = path.dirname(workspace); const gitManager: GitManagerShape = { status: vi.fn(() => Effect.void as any), @@ -2732,7 +2732,7 @@ describe("WebSocket Server", () => { const addr = server.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; - const authorizedWs = await connectWs(port, "secret-token", "cut3://app"); + const authorizedWs = await connectWs(port, "secret-token", "rowl://app"); connections.push(authorizedWs); const welcome = await waitForPush(authorizedWs, WS_CHANNELS.serverWelcome); expect(welcome.channel).toBe(WS_CHANNELS.serverWelcome); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index d56820326fe..b5ac99d0406 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -151,7 +151,7 @@ export interface ServerShape { /** * Server - Service tag for HTTP/WebSocket lifecycle management. */ -export class Server extends ServiceMap.Service()("cut3/wsServer/Server") {} +export class Server extends ServiceMap.Service()("rowl/wsServer/Server") {} const isServerNotRunningError = (error: Error): boolean => { const maybeCode = (error as NodeJS.ErrnoException).code; @@ -1350,6 +1350,27 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< return { relativePath: target.relativePath }; } + case WS_METHODS.projectsDeleteFile: { + const body = stripRequestTag(request.body); + const authorizedWorkspaceRoot = yield* authorizePath({ + requestedPath: body.cwd, + operation: "Project file delete", + }); + const target = yield* resolveWorkspaceWritePath({ + workspaceRoot: authorizedWorkspaceRoot, + relativePath: body.relativePath, + path, + }); + yield* Effect.tryPromise({ + try: () => NodeFs.promises.unlink(target.absolutePath), + catch: (cause) => + new RouteRequestError({ + message: `Failed to delete workspace file: ${String(cause)}`, + }), + }); + return {}; + } + case WS_METHODS.threadsGetShareStatus: { const body = stripRequestTag(request.body); yield* resolveLiveThreadContext(body.threadId); @@ -1850,6 +1871,32 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< return { keybindings: keybindingsConfig, issues: [] }; } + case WS_METHODS.featuresCreate: + case WS_METHODS.featuresGet: + case WS_METHODS.featuresListByProject: + case WS_METHODS.featuresUpdate: + case WS_METHODS.featuresUpdateStage: + case WS_METHODS.featuresDelete: + case WS_METHODS.goalsCreate: + case WS_METHODS.goalsGet: + case WS_METHODS.goalsListByProject: + case WS_METHODS.goalsSetMain: + case WS_METHODS.goalsLinkThread: + case WS_METHODS.goalsUnlinkThread: + case WS_METHODS.goalsUpdateText: + case WS_METHODS.goalsDelete: + case WS_METHODS.contextCreateNode: + case WS_METHODS.contextGetNode: + case WS_METHODS.contextListByProject: + case WS_METHODS.contextListByThread: + case WS_METHODS.contextCompressNode: + case WS_METHODS.contextRestoreNode: + case WS_METHODS.contextDeleteNode: { + return yield* new RouteRequestError({ + message: `Method ${request.body._tag} not yet implemented`, + }); + } + default: { const _exhaustiveCheck: never = request.body; return yield* new RouteRequestError({ diff --git a/apps/web/index.html b/apps/web/index.html index 2e9a56c7023..a592730c8fc 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -15,7 +15,7 @@ (() => { const root = document.documentElement; try { - const raw = localStorage.getItem("cut3:app-settings:v1"); + const raw = localStorage.getItem("rowl:app-settings:v1"); if (!raw) { return; } @@ -29,7 +29,7 @@ } })(); - CUT3 + Rowl
diff --git a/apps/web/public/Rowlappicon.png b/apps/web/public/Rowlappicon.png new file mode 100644 index 00000000000..762d14ae76c Binary files /dev/null and b/apps/web/public/Rowlappicon.png differ diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png index 42e669af355..762d14ae76c 100644 Binary files a/apps/web/public/apple-touch-icon.png and b/apps/web/public/apple-touch-icon.png differ diff --git a/apps/web/public/favicon-16x16.png b/apps/web/public/favicon-16x16.png index 66edce332b9..d0faa59576e 100644 Binary files a/apps/web/public/favicon-16x16.png and b/apps/web/public/favicon-16x16.png differ diff --git a/apps/web/public/favicon-32x32.png b/apps/web/public/favicon-32x32.png index f45be08fe39..ec749bc3a24 100644 Binary files a/apps/web/public/favicon-32x32.png and b/apps/web/public/favicon-32x32.png differ diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico index 8e0add12c2e..762d14ae76c 100644 Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/favicon.png b/apps/web/public/favicon.png new file mode 100644 index 00000000000..762d14ae76c Binary files /dev/null and b/apps/web/public/favicon.png differ diff --git a/apps/web/public/icon.png b/apps/web/public/icon.png index 7deef34a342..762d14ae76c 100644 Binary files a/apps/web/public/icon.png and b/apps/web/public/icon.png differ diff --git a/apps/web/public/rowl-logo-dark.png b/apps/web/public/rowl-logo-dark.png new file mode 100644 index 00000000000..3ef8baded84 Binary files /dev/null and b/apps/web/public/rowl-logo-dark.png differ diff --git a/apps/web/public/rowl-logo-light.png b/apps/web/public/rowl-logo-light.png new file mode 100644 index 00000000000..7271d9ab5e9 Binary files /dev/null and b/apps/web/public/rowl-logo-light.png differ diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 2a0c9fe8f88..02053bdeebf 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -23,7 +23,7 @@ import { CUSTOM_THEME_IDS } from "./lib/customThemes"; import { normalizeModelPreferenceSlugs } from "./lib/modelPreferences"; import { isOpenRouterGuaranteedFreeSlug } from "./lib/openRouterModels"; -const APP_SETTINGS_STORAGE_KEY = "cut3:app-settings:v1"; +const APP_SETTINGS_STORAGE_KEY = "rowl:app-settings:v1"; const MAX_CUSTOM_MODEL_COUNT = 32; const MAX_FAVORITE_MODEL_COUNT = 32; const MAX_RECENT_MODEL_COUNT = 12; @@ -69,6 +69,15 @@ const PROVIDERS_WITH_CUSTOM_MODEL_SUPPORT = new Set([ "opencode", "pi", ]); +const VALID_HIDDEN_PROVIDER_PICKER_KINDS = new Set([ + "codex", + "openrouter", + "copilot", + "kimi", + "opencode", + "pi", +]); +const MAX_HIDDEN_PROVIDER_COUNT = 8; const AppearanceThemeConfigSchema = Schema.Struct({ accent: Schema.String.check(Schema.isMaxLength(32)), background: Schema.String.check(Schema.isMaxLength(32)), @@ -235,6 +244,9 @@ const AppSettingsSchema = Schema.Struct({ hiddenPiModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), + hiddenProviders: Schema.Array(Schema.String).pipe( + Schema.withConstructorDefault(() => Option.some([])), + ), approvalRules: Schema.Array(ApprovalRuleSchema).pipe( Schema.withConstructorDefault(() => Option.some([])), ), @@ -337,6 +349,29 @@ export function normalizeModelVisibilitySlugs( ); } +function normalizeHiddenProviders(providers: Iterable): string[] { + const normalizedProviders: string[] = []; + const seen = new Set(); + + for (const candidate of providers) { + if (typeof candidate !== "string") { + continue; + } + const trimmed = candidate.trim(); + if (!trimmed || seen.has(trimmed) || !VALID_HIDDEN_PROVIDER_PICKER_KINDS.has(trimmed)) { + continue; + } + + seen.add(trimmed); + normalizedProviders.push(trimmed); + if (normalizedProviders.length >= MAX_HIDDEN_PROVIDER_COUNT) { + break; + } + } + + return normalizedProviders; +} + function normalizeAppSettings(settings: AppSettings): AppSettings { const customThemeId = settings.customThemeId === "none" && settings.enableCatppuccinTheme @@ -412,6 +447,7 @@ function normalizeAppSettings(settings: AppSettings): AppSettings { hiddenOpencodeModels: normalizeModelVisibilitySlugs(settings.hiddenOpencodeModels, "opencode"), hiddenKimiModels: normalizeModelVisibilitySlugs(settings.hiddenKimiModels, "kimi"), hiddenPiModels: normalizeModelVisibilitySlugs(settings.hiddenPiModels, "pi"), + hiddenProviders: normalizeHiddenProviders(settings.hiddenProviders), approvalRules: normalizeApprovalRules(settings.approvalRules), }; } diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts index c9b41e6f5c8..07adb1d175f 100644 --- a/apps/web/src/branding.ts +++ b/apps/web/src/branding.ts @@ -1,6 +1,6 @@ import { resolveAppReleaseBranding } from "@t3tools/shared/appRelease"; -export const APP_BASE_NAME = "CUT3"; +export const APP_BASE_NAME = "Rowl"; export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; const appReleaseBranding = resolveAppReleaseBranding({ version: APP_VERSION, diff --git a/apps/web/src/components/AppearanceSettingsSection.tsx b/apps/web/src/components/AppearanceSettingsSection.tsx index e46e0bca32a..a98b00d835b 100644 --- a/apps/web/src/components/AppearanceSettingsSection.tsx +++ b/apps/web/src/components/AppearanceSettingsSection.tsx @@ -71,7 +71,7 @@ function getAppearanceCopy(language: AppLanguage) { return { sectionTitle: "ظاهر", sectionDescription: - "پالت پایه روشن و تیره، تایپوگرافی و کنترل های ظاهری CUT3 را برای وب و Electron تنظیم کنید.", + "پالت پایه روشن و تیره، تایپوگرافی و کنترل های ظاهری Rowl را برای وب و Electron تنظیم کنید.", themeMode: { light: { label: "روشن", description: "از ظاهر روشن استفاده شود." }, dark: { label: "تیره", description: "از ظاهر تیره استفاده شود." }, @@ -107,7 +107,7 @@ function getAppearanceCopy(language: AppLanguage) { pointerCursorsDescription: "به جای فلش پیش فرض، روی دکمه ها و پیوندها از نشانگر دستی استفاده شود.", uiFontSize: "اندازه فونت رابط", - uiFontSizeDescription: "اندازه پایه مورد استفاده در رابط CUT3 را تنظیم می کند.", + uiFontSizeDescription: "اندازه پایه مورد استفاده در رابط Rowl را تنظیم می کند.", uiFontSizeAria: "اندازه فونت رابط بر حسب پیکسل", timestampFormat: "قالب زمان", timestampFormatDescription: @@ -136,7 +136,7 @@ function getAppearanceCopy(language: AppLanguage) { importDialogDescription: "یک شیء JSON با فیلدهای accent، background، foreground، uiFont، codeFont، translucentSidebar و contrast وارد کنید.", importDialogFootnote: (themeName: string) => - `CUT3 فقط مقادیر ${themeName} فعلی را درون ریزی می کند.`, + `Rowl فقط مقادیر ${themeName} فعلی را درون ریزی می کند.`, cancel: "لغو", applyImport: "اعمال درون ریزی", }; @@ -145,7 +145,7 @@ function getAppearanceCopy(language: AppLanguage) { return { sectionTitle: "Appearance", sectionDescription: - "Customize CUT3's base light and dark palettes, typography, and interactive chrome across web and Electron.", + "Customize Rowl's base light and dark palettes, typography, and interactive chrome across web and Electron.", themeMode: { light: { label: "Light", description: "Use the light appearance." }, dark: { label: "Dark", description: "Use the dark appearance." }, @@ -181,7 +181,7 @@ function getAppearanceCopy(language: AppLanguage) { pointerCursorsDescription: "Use hand cursors on buttons and links instead of the default arrow.", uiFontSize: "UI font size", - uiFontSizeDescription: "Adjust the base size used across the shared CUT3 interface.", + uiFontSizeDescription: "Adjust the base size used across the shared Rowl interface.", uiFontSizeAria: "UI font size in pixels", timestampFormat: "Timestamp format", timestampFormatDescription: "System default follows your browser or OS time format.", @@ -207,7 +207,7 @@ function getAppearanceCopy(language: AppLanguage) { importDialogDescription: "Paste a JSON object with accent, background, foreground, uiFont, codeFont, translucentSidebar, and contrast.", importDialogFootnote: (themeName: string) => - `CUT3 only imports the current ${themeName} values.`, + `Rowl only imports the current ${themeName} values.`, cancel: "Cancel", applyImport: "Apply import", }; diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index 10510c1858d..66d257774f5 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -102,17 +102,17 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => { worktreePath: null, }, { - name: "CUT3/feature/demo", + name: "Rowl/feature/demo", isRemote: true, - remoteName: "CUT3", + remoteName: "Rowl", current: false, isDefault: false, worktreePath: null, }, { - name: "CUT3/feature/remote-only", + name: "Rowl/feature/remote-only", isRemote: true, - remoteName: "CUT3", + remoteName: "Rowl", current: false, isDefault: false, worktreePath: null, @@ -121,7 +121,7 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => { expect(dedupeRemoteBranchesWithLocalMatches(input).map((branch) => branch.name)).toEqual([ "feature/demo", - "CUT3/feature/remote-only", + "Rowl/feature/remote-only", ]); }); @@ -134,9 +134,9 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => { worktreePath: null, }, { - name: "CUT3/feature/remote-only", + name: "Rowl/feature/remote-only", isRemote: true, - remoteName: "CUT3", + remoteName: "Rowl", current: false, isDefault: false, worktreePath: null, @@ -145,11 +145,11 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => { expect(dedupeRemoteBranchesWithLocalMatches(input).map((branch) => branch.name)).toEqual([ "feature/local", - "CUT3/feature/remote-only", + "Rowl/feature/remote-only", ]); }); - it("keeps non-CUT3 remote refs visible even when a matching local branch exists", () => { + it("keeps non-Rowl remote refs visible even when a matching local branch exists", () => { const input: GitBranch[] = [ { name: "feature/demo", @@ -173,7 +173,7 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => { ]); }); - it("keeps non-CUT3 remote refs visible when git tracks with first-slash local naming", () => { + it("keeps non-Rowl remote refs visible when git tracks with first-slash local naming", () => { const input: GitBranch[] = [ { name: "upstream/feature", diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index 6e569ce13f5..9a9f0890854 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -1,7 +1,7 @@ import type { GitBranch } from "@t3tools/contracts"; export type EnvMode = "local" | "worktree"; -const PREFERRED_REMOTE_NAME = "CUT3"; +const PREFERRED_REMOTE_NAME = "Rowl"; export function resolveEffectiveEnvMode(input: { activeWorktreePath: string | null; diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index faa9dc7d1d7..c1fb98af877 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -38,7 +38,7 @@ const PROJECT_ID = "project-1" as ProjectId; const NOW_ISO = "2026-03-04T12:00:00.000Z"; const BASE_TIME_MS = Date.parse(NOW_ISO); const ATTACHMENT_SVG = ""; -const APP_SETTINGS_STORAGE_KEY = "cut3:app-settings:v1"; +const APP_SETTINGS_STORAGE_KEY = "rowl:app-settings:v1"; const CHAT_BACKGROUND_TEST_DATA_URL = "data:image/svg+xml," + encodeURIComponent( @@ -134,7 +134,7 @@ function isoAt(offsetSeconds: number): string { function createBaseServerConfig(): ServerConfig { return { cwd: "/repo/project", - keybindingsConfigPath: "/repo/project/.cut3-keybindings.json", + keybindingsConfigPath: "/repo/project/.rowl-keybindings.json", keybindings: [], issues: [], providers: [ @@ -311,6 +311,7 @@ function createSnapshotForTargetUser(options: { id: THREAD_ID, projectId: PROJECT_ID, title: "Browser test thread", + goal: null, model: "gpt-5.4", interactionMode: "default", runtimeMode: "full-access", @@ -473,6 +474,7 @@ function addThreadToSnapshot( id: threadId, projectId: PROJECT_ID, title: "New thread", + goal: null, model: "gpt-5.4", interactionMode: "default", runtimeMode: "full-access", diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 5ffca53cc3f..c6d1152cfa8 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -10,8 +10,8 @@ import { getAppModelOptions } from "../appSettings"; import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore"; import { Schema } from "effect"; -export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "cut3:last-invoked-script-by-project"; -const WORKTREE_BRANCH_PREFIX = "cut3"; +export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "rowl:last-invoked-script-by-project"; +const WORKTREE_BRANCH_PREFIX = "rowl"; export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); @@ -26,6 +26,7 @@ export function buildLocalDraftThread( codexThreadId: null, projectId: draftThread.projectId, title: "New thread", + goal: null, model: fallbackModel, runtimeMode: draftThread.runtimeMode, interactionMode: draftThread.interactionMode, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 4a83c0a4f6f..2b21e56feab 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -136,6 +136,7 @@ import { getProviderPickerBackingProvider, getProviderPickerKindForSelection, findLatestProposedPlan, + PROVIDER_OPTIONS, type AvailableProviderPickerKind, type LatestModelRerouteNotice, type PendingApproval, @@ -253,6 +254,7 @@ import { toastManager } from "./ui/toast"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; import ProjectScriptsControl, { type NewProjectScriptInput } from "./ProjectScriptsControl"; import { MessagesTimeline } from "./chat/MessagesTimeline"; +import { ThreadGoalStatement } from "./chat/ThreadGoalStatement"; import { ProviderModelPicker, PROVIDER_ICON_BY_PROVIDER, @@ -274,8 +276,10 @@ import { setupProjectScript, } from "~/projectScripts"; import { Toggle } from "./ui/toggle"; +import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "./ui/collapsible"; import ThreadNewButton from "./ThreadNewButton"; import ThreadSidebarToggle from "./ThreadSidebarToggle"; +import RightSidebarToggle from "./RightSidebarToggle"; import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; import { readNativeApi } from "~/nativeApi"; import { @@ -338,8 +342,8 @@ import { findMatchingApprovalRule, type ApprovalRule } from "../approvalRules"; import { formatTimestamp } from "../timestampFormat"; import { showTurnCompleteNotification } from "../notifications"; -const LAST_EDITOR_KEY = "cut3:last-editor"; -const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "cut3:last-invoked-script-by-project"; +const LAST_EDITOR_KEY = "rowl:last-editor"; +const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "rowl:last-invoked-script-by-project"; const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000; const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; const IMAGE_ONLY_BOOTSTRAP_PROMPT = @@ -366,7 +370,7 @@ const EMPTY_PENDING_USER_INPUT_ANSWERS: Record { + if (hidden) { + return { hiddenProviders: [...new Set([...hiddenProviders, provider])] }; + } + return { hiddenProviders: hiddenProviders.filter((p) => p !== provider) }; +} + function filterHiddenModelOptions( options: ReadonlyArray, hiddenModels: readonly string[], @@ -660,6 +675,7 @@ function buildLocalDraftThread( codexThreadId: null, projectId: draftThread.projectId, title: "New thread", + goal: null, model: fallbackModel, runtimeMode: draftThread.runtimeMode, interactionMode: draftThread.interactionMode, @@ -1688,6 +1704,35 @@ export default function ChatView({ threadId }: ChatViewProps) { ); const selectedCodexSupportsFastMode = selectedProvider === "codex" && !selectedModelUsesOpenRouter; + const selectedModelSupportsImages = useMemo(() => { + if (selectedProvider === "opencode") { + return false; + } + if (selectedProvider === "codex" && selectedModelUsesOpenRouter) { + if (selectedOpenRouterModel) { + return selectedOpenRouterModel.supportsImages; + } + const codexModelOption = allModelOptionsByProvider.codex.find( + (m) => m.slug === selectedModel, + ); + return codexModelOption?.supportsImageInput ?? false; + } + if (selectedProvider === "pi") { + return providerStatusModelOptionsByProvider.pi.some( + (m) => m.slug === selectedModel && m.supportsImageInput, + ); + } + const providerModels = allModelOptionsByProvider[selectedProvider]; + const modelOption = providerModels.find((m) => m.slug === selectedModel); + return modelOption?.supportsImageInput ?? false; + }, [ + selectedProvider, + selectedModel, + selectedModelUsesOpenRouter, + selectedOpenRouterModel, + providerStatusModelOptionsByProvider.pi, + allModelOptionsByProvider, + ]); const copilotReasoningProbeQuery = useQuery( serverCopilotReasoningProbeQueryOptions( { @@ -2013,7 +2058,7 @@ export default function ChatView({ threadId }: ChatViewProps) { if (openRouterModel === null) { message = `\`${input.model}\` is not in OpenRouter's current live free catalog. Refresh the list, pick another listed free model, or use \`openrouter/free\`.`; } else if (!supportsOpenRouterNativeToolCalling(openRouterModel)) { - message = `${openRouterModel.name} does not advertise the full OpenRouter native tool-calling surface (\`tools\` + \`tool_choice\`), and CUT3 requires both for agent turns. Switch to another OpenRouter free model or use \`openrouter/free\`.`; + message = `${openRouterModel.name} does not advertise the full OpenRouter native tool-calling surface (\`tools\` + \`tool_choice\`), and Rowl requires both for agent turns. Switch to another OpenRouter free model or use \`openrouter/free\`.`; } if (message === null) { return true; @@ -2075,8 +2120,9 @@ export default function ChatView({ threadId }: ChatViewProps) { () => AVAILABLE_PROVIDER_OPTIONS.filter( (option) => - lockedProvider === null || - getProviderPickerBackingProvider(option.value) === lockedProvider, + !settings.hiddenProviders.includes(option.value) && + (lockedProvider === null || + getProviderPickerBackingProvider(option.value) === lockedProvider), ).flatMap((option) => { const backingProvider = getProviderPickerBackingProvider(option.value) ?? "codex"; return prioritizeModelOptions( @@ -2100,6 +2146,7 @@ export default function ChatView({ threadId }: ChatViewProps) { }), [ lockedProvider, + settings.hiddenProviders, visibleModelOptionsByProvider, visibleOpenRouterModelOptions, visibleOpencodeModelOptions, @@ -2113,8 +2160,9 @@ export default function ChatView({ threadId }: ChatViewProps) { providerHiddenModelSettings.hiddenCopilotModels.length > 0 || providerHiddenModelSettings.hiddenOpencodeModels.length > 0 || providerHiddenModelSettings.hiddenKimiModels.length > 0 || - providerHiddenModelSettings.hiddenPiModels.length > 0, - [providerHiddenModelSettings], + providerHiddenModelSettings.hiddenPiModels.length > 0 || + settings.hiddenProviders.length > 0, + [providerHiddenModelSettings, settings.hiddenProviders], ); const phase = derivePhase(activeThread?.session ?? null); const isConnecting = phase === "connecting"; @@ -4231,6 +4279,17 @@ export default function ChatView({ threadId }: ChatViewProps) { const addComposerImages = (files: File[]) => { if (!activeThreadId || files.length === 0) return; + if (!selectedModelSupportsImages) { + toastManager.add({ + type: "error", + title: + selectedProvider === "opencode" + ? "Image attachments not supported with OpenCode" + : "Image attachments not supported by current model", + }); + return; + } + if (pendingUserInputs.length > 0) { toastManager.add({ type: "error", @@ -4574,7 +4633,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const confirmed = await api.dialogs.confirm( [ "Compact this thread?", - "CUT3 will replace the live provider session with a continuation summary so the conversation can keep going from a smaller context.", + "Rowl will replace the live provider session with a continuation summary so the conversation can keep going from a smaller context.", ].join("\n"), ); if (!confirmed) { @@ -6068,6 +6127,12 @@ export default function ChatView({ threadId }: ChatViewProps) { }, [providerHiddenModelSettings, updateSettings], ); + const onProviderVisibilityChange = useCallback( + (provider: AvailableProviderPickerKind, visible: boolean) => { + updateSettings(patchHiddenProviders(settings.hiddenProviders, provider, !visible)); + }, + [settings.hiddenProviders, updateSettings], + ); const showAllManagedModels = useCallback(() => { updateSettings({ hiddenCodexModels: [], @@ -6075,6 +6140,7 @@ export default function ChatView({ threadId }: ChatViewProps) { hiddenOpencodeModels: [], hiddenKimiModels: [], hiddenPiModels: [], + hiddenProviders: [], }); }, [updateSettings]); useEffect(() => { @@ -6694,6 +6760,11 @@ export default function ChatView({ threadId }: ChatViewProps) { timestampFormat={timestampFormat} checkpointTurnCountByTurnId={inferredCheckpointTurnCountByTurnId} /> +
{/* Messages */}
setIsUsageDashboardOpen(true)} onProviderModelChange={onProviderModelSelectFromPicker} + onSetAsDefault={(provider, model) => { + if (!activeProject) return; + const api = readNativeApi(); + if (!api) return; + void api.orchestration.dispatchCommand({ + type: "project.meta.update", + commandId: newCommandId(), + projectId: activeProject.id, + defaultModel: model, + }); + }} /> composerFileInputRef.current?.click()} /> } > - {chatCopy.attachImagesTooltip} + + {selectedModelSupportsImages + ? chatCopy.attachImagesTooltip + : selectedProvider === "opencode" + ? "Current OpenCode model does not support image attachments" + : chatCopy.attachImagesNotSupported} + ) : null} {isPreparingWorktree ? ( @@ -7829,6 +7919,7 @@ export default function ChatView({ threadId }: ChatViewProps) { openRouterModelOptions={openRouterModelOptions} opencodeModelOptions={opencodeModelOptions} hiddenModelsByProvider={providerHiddenModelSettings} + hiddenProviders={settings.hiddenProviders} favoriteModelsByProvider={providerFavoriteModelSettings} recentModelsByProvider={providerRecentModelSettings} openRouterContextLengthsBySlug={openRouterContextLengthsBySlug} @@ -7836,6 +7927,7 @@ export default function ChatView({ threadId }: ChatViewProps) { serviceTierSetting={selectedServiceTierSetting} onFavoriteModelChange={onFavoriteModelChange} onModelVisibilityChange={onModelVisibilityChange} + onProviderVisibilityChange={onProviderVisibilityChange} onShowAll={showAllManagedModels} onOpenProviderSetup={openProviderSetupFromManageModels} /> @@ -7855,7 +7947,7 @@ export default function ChatView({ threadId }: ChatViewProps) { Enter your OpenRouter API key - CUT3 needs an OpenRouter API key before it can start Codex sessions that use + Rowl needs an OpenRouter API key before it can start Codex sessions that use OpenRouter-routed models such as openrouter/free or specific{" "} :free model ids. @@ -7923,7 +8015,7 @@ export default function ChatView({ threadId }: ChatViewProps) { Enter your Kimi API key - CUT3 can start Kimi CLI chat with a Kimi Code API key. You can generate one from the + Rowl can start Kimi CLI chat with a Kimi Code API key. You can generate one from the Kimi Code Console, or authenticate in the local CLI with kimi login or /login instead. @@ -8189,6 +8281,7 @@ const ChatHeader = memo(function ChatHeader({ : "Toggle diff panel"} +
); @@ -8392,8 +8485,8 @@ const ThreadModelRerouteBanner = memo(function ThreadModelRerouteBanner({ : getModelDisplayName(notice.toModel, "codex"); const message = notice.toModel === "openrouter/free" - ? `CUT3 retried this turn through ${toModelLabel} after ${fromModelLabel} could not be served. OpenRouter may answer with a different free model for this turn.` - : `CUT3 retried this turn from ${fromModelLabel} to ${toModelLabel}.`; + ? `Rowl retried this turn through ${toModelLabel} after ${fromModelLabel} could not be served. OpenRouter may answer with a different free model for this turn.` + : `Rowl retried this turn from ${fromModelLabel} to ${toModelLabel}.`; return (
@@ -8775,7 +8868,7 @@ function getCustomModelOptionsByProvider( return { codex: mergeModelOptions( - providerStatusModelsByProvider.codex, + mergeModelOptions(providerStatusModelsByProvider.codex, configuredModelsByProvider.codex), getAppModelOptions("codex", settings.customCodexModels), ), copilot: mergeModelOptions( @@ -8826,10 +8919,11 @@ function getChatSurfaceCopy(language: AppLanguage) { attachImages: "پیوست کردن تصاویر", attachImagesTooltip: "تصاویر را انتخاب، رها، یا پیست کنید؛ حداکثر ۸ تصویر و هر کدام تا ۱۰ مگابایت", + attachImagesNotSupported: "مدل فعلی از تصاویر پشتیبانی نمی‌کند", followUpQueued: "پیگیری در صف قرار گرفت", steeringCurrentRun: "در حال هدایت نوبت فعلی", - steeringCurrentRunHint: "CUT3 نوبت فعلی را متوقف می کند و این پیگیری را بعدی می فرستد.", - queueCurrentRunHint: "CUT3 این پیگیری را بعد از تمام شدن نوبت فعلی می فرستد.", + steeringCurrentRunHint: "Rowl نوبت فعلی را متوقف می کند و این پیگیری را بعدی می فرستد.", + queueCurrentRunHint: "Rowl این پیگیری را بعد از تمام شدن نوبت فعلی می فرستد.", queuedTurnFailed: "ارسال مورد در صف انجام نشد", retryQueuedFollowUp: "تلاش دوباره", removeQueuedFollowUp: "حذف", @@ -8883,10 +8977,11 @@ function getChatSurfaceCopy(language: AppLanguage) { queuedFollowUpsFailedHint: "Retry or remove failed items before sending more follow-ups.", attachImages: "Attach images", attachImagesTooltip: "Attach images · drag, paste, or pick up to 8 images (10 MB each)", + attachImagesNotSupported: "Images not supported by current model", followUpQueued: "Follow-up queued", steeringCurrentRun: "Steering current run", - steeringCurrentRunHint: "CUT3 will stop the current turn and send this follow-up next.", - queueCurrentRunHint: "CUT3 will send this follow-up after the current turn settles.", + steeringCurrentRunHint: "Rowl will stop the current turn and send this follow-up next.", + queueCurrentRunHint: "Rowl will send this follow-up after the current turn settles.", queuedTurnFailed: "Queued follow-up failed", retryQueuedFollowUp: "Retry", removeQueuedFollowUp: "Remove", @@ -9134,7 +9229,7 @@ export function ProviderSetupDialog(props: { "وضعیت runtime های محلی را بررسی کنید، کلیدها را اضافه کنید، و قدم بعدی هر ارائه دهنده را بدون خروج از چت ببینید.", snapshotTitle: "نمای آماده سازی ارائه دهنده", snapshotDescription: - "CUT3 وضعیت runtime های محلی را می خواند. احراز هویت OpenCode، Codex، Copilot، Kimi، و Pi همچنان در ابزارهای خود آنها مدیریت می شود.", + "Rowl وضعیت runtime های محلی را می خواند. احراز هویت OpenCode، Codex، Copilot، Kimi، و Pi همچنان در ابزارهای خود آنها مدیریت می شود.", ready: "آماده", attention: "نیاز به توجه", unavailable: "ناموجود", @@ -9158,7 +9253,7 @@ export function ProviderSetupDialog(props: { "Check local runtime health, add keys, and see the next step for each provider without leaving chat.", snapshotTitle: "Provider readiness snapshot", snapshotDescription: - "CUT3 inspects your local runtimes here. Authentication for OpenCode, Codex, Copilot, Kimi, and Pi still lives in their own CLIs and config files.", + "Rowl inspects your local runtimes here. Authentication for OpenCode, Codex, Copilot, Kimi, and Pi still lives in their own CLIs and config files.", ready: "Ready", attention: "Needs attention", unavailable: "Unavailable", @@ -9306,7 +9401,7 @@ export function ProviderSetupDialog(props: { ? "Shared OpenRouter key is ready for OpenRouter-routed sessions." : "Add the shared OpenRouter API key to unlock OpenRouter-routed models."; message = - "Used for openrouter/free and any saved OpenRouter :free slugs. CUT3 also forwards the same key to new OpenCode sessions when their config expects OPENROUTER_API_KEY."; + "Used for openrouter/free and any saved OpenRouter :free slugs. Rowl also forwards the same key to new OpenCode sessions when their config expects OPENROUTER_API_KEY."; actions.push( + ) : ( + 0 ? "warning" : "outline"} + size="sm" + > + {section.hiddenCount > 0 + ? `${section.filteredOptions.length - section.hiddenCount} ${copy.shown.toLowerCase()} · ${section.hiddenCount} ${copy.hidden.toLowerCase()}` + : `${section.filteredOptions.length} ${copy.shown.toLowerCase()}`} + + )} + {!section.providerHidden && section.filteredOptions.length > 0 ? ( + + ) : null} +
+ + {section.filteredOptions.map((modelOption) => { const displayParts = getModelPickerOptionDisplayParts(modelOption); const contextLabel = getModelOptionContextLabel( @@ -9799,10 +9945,11 @@ function ManageModelsDialog(props: { props.openRouterContextLengthsBySlug, props.opencodeContextLengthsBySlug, ); - const visible = !getHiddenModelsForProvider( + const individualHidden = getHiddenModelsForProvider( section.backingProvider, props.hiddenModelsByProvider, ).includes(modelOption.slug); + const visible = !section.providerHidden && !individualHidden; return (
- {visible ? copy.shown : copy.hidden} + {section.providerHidden + ? copy.hidden + : visible + ? copy.shown + : copy.hidden} { props.onModelVisibilityChange( section.backingProvider, @@ -9892,8 +10044,8 @@ function ManageModelsDialog(props: {
); })} - - +
+ ); })} diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 3a0e4fab279..e6dc549a9b2 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -1,7 +1,7 @@ /** * CommandPalette - Global searchable command overlay. * - * Opens with Ctrl+Shift+P / Cmd+Shift+P and provides quick access to + * Opens with Ctrl+K / Cmd+K and provides quick access to * all major app actions with their keyboard shortcuts displayed. */ import { type KeybindingCommand, type ResolvedKeybindingsConfig } from "@t3tools/contracts"; @@ -9,7 +9,9 @@ import { FilePlusIcon, FolderPlusIcon, LayoutDashboardIcon, + Minimize2Icon, PanelLeftIcon, + SearchIcon, SettingsIcon, SquareTerminalIcon, CodeIcon, @@ -185,4 +187,6 @@ export const PALETTE_ICONS = { settings: , openInEditor: , notifications: , + search: , + compact: , } as const; diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 17493173754..840b03781c6 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -652,7 +652,7 @@ describe("when: branch has no upstream configured", () => { assert.deepEqual(quick, { kind: "show_hint", label: "Push", - hint: 'Add a "CUT3" remote before pushing or creating a PR.', + hint: 'Add a "Rowl" remote before pushing or creating a PR.', disabled: true, }); }); diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 8c7c44ef352..d7332afcb36 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -36,7 +36,7 @@ export type DefaultBranchConfirmableAction = "commit_push" | "commit_push_pr"; const SHORT_SHA_LENGTH = 7; const TOAST_DESCRIPTION_MAX = 72; -const PREFERRED_REMOTE_NAME = "CUT3"; +const PREFERRED_REMOTE_NAME = "Rowl"; function getGitActionLogicCopy(language: AppLanguage) { if (language === "fa") { diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 90fe794f677..01eb3005103 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -63,7 +63,7 @@ interface PendingDefaultBranchAction { } type GitActionToastId = ReturnType; -const PREFERRED_REMOTE_NAME = "CUT3"; +const PREFERRED_REMOTE_NAME = "Rowl"; function getGitActionsUiCopy(language: AppLanguage) { if (language === "fa") { diff --git a/apps/web/src/components/KeybindingsToast.browser.tsx b/apps/web/src/components/KeybindingsToast.browser.tsx index a0c951c96e6..14823575db9 100644 --- a/apps/web/src/components/KeybindingsToast.browser.tsx +++ b/apps/web/src/components/KeybindingsToast.browser.tsx @@ -41,7 +41,7 @@ const wsLink = ws.link(/ws(s)?:\/\/.*/); function createBaseServerConfig(): ServerConfig { return { cwd: "/repo/project", - keybindingsConfigPath: "/repo/project/.cut3-keybindings.json", + keybindingsConfigPath: "/repo/project/.rowl-keybindings.json", keybindings: [ { command: "commandPalette.toggle", @@ -144,6 +144,7 @@ function createMinimalSnapshot(): OrchestrationReadModel { id: THREAD_ID, projectId: PROJECT_ID, title: "Test thread", + goal: null, model: "gpt-5", interactionMode: "default", runtimeMode: "full-access", diff --git a/apps/web/src/components/OpenCodeCredentialsManager.tsx b/apps/web/src/components/OpenCodeCredentialsManager.tsx index 70479ce7aa8..26a07cb398e 100644 --- a/apps/web/src/components/OpenCodeCredentialsManager.tsx +++ b/apps/web/src/components/OpenCodeCredentialsManager.tsx @@ -127,7 +127,7 @@ export const OpenCodeCredentialsManager = memo(function OpenCodeCredentialsManag

OpenCode runtime

- CUT3 reads {binaryCommand} auth list, {binaryCommand} mcp list, + Rowl reads {binaryCommand} auth list, {binaryCommand} mcp list, and {binaryCommand} mcp auth list. OpenCode still owns the actual login, logout, and OAuth flows.

@@ -256,7 +256,7 @@ export const OpenCodeCredentialsManager = memo(function OpenCodeCredentialsManag {!mcpSupported ? (

- CUT3 could not inspect OpenCode MCP servers from this runtime snapshot. + Rowl could not inspect OpenCode MCP servers from this runtime snapshot.

) : mcpServers.length === 0 ? (

diff --git a/apps/web/src/components/PiProvider.browser.tsx b/apps/web/src/components/PiProvider.browser.tsx index 4b705eb65a3..380afe88318 100644 --- a/apps/web/src/components/PiProvider.browser.tsx +++ b/apps/web/src/components/PiProvider.browser.tsx @@ -61,7 +61,7 @@ describe("Pi provider GUI", () => { authStatus: "unauthenticated", checkedAt: "2026-03-24T00:00:00.000Z", message: - "Pi is embedded in CUT3, but no authenticated Pi-backed models are currently available.", + "Pi is embedded in Rowl, but no authenticated Pi-backed models are currently available.", }, ]; @@ -93,13 +93,13 @@ describe("Pi provider GUI", () => { const piCard = await waitForElement( () => Array.from(document.querySelectorAll("div, section")).find((element) => - (element.textContent ?? "").includes("CUT3 embeds Pi through its Node SDK"), + (element.textContent ?? "").includes("Rowl embeds Pi through its Node SDK"), ) ?? null, "Unable to find the Pi guidance card in the provider setup dialog.", ); expect(piCard.textContent).toContain("Pi"); - expect(piCard.textContent).toContain("CUT3 embeds Pi through its Node SDK"); + expect(piCard.textContent).toContain("Rowl embeds Pi through its Node SDK"); expect(piCard.textContent).toContain("bunx pi"); } finally { await screen.unmount(); @@ -145,12 +145,15 @@ describe("Pi provider GUI", () => { opencodeContextLengthsBySlug={new Map()} serviceTierSetting="auto" hasHiddenModels={false} + hiddenProviders={[]} favoriteModelsByProvider={{ codex: [], copilot: [], kimi: [], opencode: [], pi: [] }} recentModelsByProvider={{ codex: [], copilot: [], kimi: [], opencode: [], pi: [] }} + projectDefaultModel={null} onOpenProviderSetup={() => undefined} onOpenManageModels={() => undefined} onOpenUsageDashboard={() => undefined} onProviderModelChange={onProviderModelChange} + onSetAsDefault={vi.fn()} /> , ); @@ -241,12 +244,15 @@ describe("Pi provider GUI", () => { opencodeContextLengthsBySlug={new Map()} serviceTierSetting="auto" hasHiddenModels={false} + hiddenProviders={[]} favoriteModelsByProvider={{ codex: [], copilot: [], kimi: [], opencode: [], pi: [] }} recentModelsByProvider={{ codex: [], copilot: [], kimi: [], opencode: [], pi: [] }} + projectDefaultModel={null} onOpenProviderSetup={() => undefined} onOpenManageModels={() => undefined} onOpenUsageDashboard={() => undefined} onProviderModelChange={onProviderModelChange} + onSetAsDefault={vi.fn()} /> , ); diff --git a/apps/web/src/components/ProjectIconDialog.tsx b/apps/web/src/components/ProjectIconDialog.tsx new file mode 100644 index 00000000000..249ff266f35 --- /dev/null +++ b/apps/web/src/components/ProjectIconDialog.tsx @@ -0,0 +1,265 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { ImageIcon, TrashIcon } from "lucide-react"; + +import { + Dialog, + DialogDescription, + DialogFooter, + DialogHeader, + DialogPanel, + DialogPopup, + DialogTitle, +} from "./ui/dialog"; +import { Button } from "./ui/button"; +import { resolveServerHttpUrl } from "~/lib/serverUrl"; + +const ICON_PATH = ".rowl/icon.png"; +const ACCEPTED_TYPES = ["image/png", "image/jpeg", "image/svg+xml", "image/x-icon"]; + +interface ProjectIconDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + projectId: string; + projectName: string; + projectCwd: string; + onSave: (iconDataUrl: string) => Promise; + onRemove: () => Promise; +} + +async function resizeImageToDataUrl(file: File, maxSize: number): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(file); + img.addEventListener("load", () => { + URL.revokeObjectURL(url); + let { width, height } = img; + if (width > maxSize || height > maxSize) { + if (width > height) { + height = Math.round((height * maxSize) / width); + width = maxSize; + } else { + width = Math.round((width * maxSize) / height); + height = maxSize; + } + } + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("Failed to create canvas context")); + return; + } + ctx.drawImage(img, 0, 0, width, height); + resolve(canvas.toDataURL("image/png")); + }); + img.addEventListener("error", () => { + URL.revokeObjectURL(url); + reject(new Error("Failed to load image")); + }); + img.src = url; + }); +} + +async function fileToDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener("load", () => resolve(reader.result as string)); + reader.addEventListener("error", () => reject(new Error("Failed to read file"))); + reader.readAsDataURL(file); + }); +} + +export function ProjectIconDialog({ + open, + onOpenChange, + projectName, + projectCwd, + onSave, + onRemove, +}: ProjectIconDialogProps) { + const [preview, setPreview] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [isRemoving, setIsRemoving] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + const [iconTimestamp, setIconTimestamp] = useState(() => Date.now()); + + const currentIconSrc = resolveServerHttpUrl( + `/api/project-favicon?cwd=${encodeURIComponent(projectCwd)}&t=${iconTimestamp}`, + ); + + const reset = useCallback(() => { + setPreview(null); + setSelectedFile(null); + setError(null); + setIsSaving(false); + setIsRemoving(false); + }, []); + + useEffect(() => { + if (!open) { + reset(); + } + }, [open, reset]); + + useEffect(() => { + if (open) { + setIconTimestamp(Date.now()); + } + }, [open]); + + const handleFileSelect = useCallback(async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!ACCEPTED_TYPES.includes(file.type)) { + setError("Please select a PNG, JPEG, SVG, or ICO file."); + return; + } + + setError(null); + + try { + let dataUrl: string; + if (file.type === "image/svg+xml" || file.type === "image/x-icon") { + dataUrl = await fileToDataUrl(file); + } else { + dataUrl = await resizeImageToDataUrl(file, 64); + } + setSelectedFile(file); + setPreview(dataUrl); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to process image."); + } + }, []); + + const handleSave = useCallback(async () => { + if (!preview) return; + setIsSaving(true); + setError(null); + try { + await onSave(preview); + setIconTimestamp(Date.now()); + onOpenChange(false); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to save icon."); + setIsSaving(false); + } + }, [preview, onSave, onOpenChange]); + + const handleRemove = useCallback(async () => { + setIsRemoving(true); + setError(null); + try { + await onRemove(); + setIconTimestamp(Date.now()); + onOpenChange(false); + } catch { + setIconTimestamp(Date.now()); + onOpenChange(false); + } + }, [onRemove, onOpenChange]); + + return ( +

+ + + Project Icon + + Choose an icon for {projectName}. The icon is saved as{" "} + {ICON_PATH} in your project. + + + + +
+
+ {preview ? ( + Icon preview + ) : ( +
+ +
+ )} +
+ +
+ Current: + { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + {ICON_PATH} +
+
+ +
+ + + {selectedFile && ( +

+ {selectedFile.name} ({(selectedFile.size / 1024).toFixed(1)} KB) +

+ )} +
+ + {error &&

{error}

} +
+ + + + + + +
+
+ ); +} diff --git a/apps/web/src/components/RightSidebarToggle.tsx b/apps/web/src/components/RightSidebarToggle.tsx new file mode 100644 index 00000000000..9df56e510cb --- /dev/null +++ b/apps/web/src/components/RightSidebarToggle.tsx @@ -0,0 +1,39 @@ +import { useMemo } from "react"; + +import { useAppSettings } from "../appSettings"; +import { PanelRightIcon, PanelRightCloseIcon } from "lucide-react"; +import { Button } from "./ui/button"; +import { useRightSidebar } from "./right-sidebar/RightSidebarContext"; +import { cn } from "../lib/utils"; + +export default function RightSidebarToggle({ className }: { className?: string }) { + const { rightSidebarExpanded, toggleRightSidebar } = useRightSidebar(); + const { + settings: { language }, + } = useAppSettings(); + const toggleLabel = useMemo(() => { + if (language === "fa") { + return "تغییر نوار کناری راست"; + } + return rightSidebarExpanded ? "Close right sidebar" : "Open right sidebar"; + }, [language, rightSidebarExpanded]); + + return ( + + ); +} diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index b3ac7a04a6e..2833f48709c 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -23,6 +23,7 @@ const baseThread = (overrides: Partial = {}): Thread => ({ codexThreadId: null, projectId: projectId("project-1"), title: "First thread", + goal: null, model: "gpt-5.4", runtimeMode: "full-access", interactionMode: "default", diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 6938d3926c8..6eba6be28d6 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -42,7 +42,8 @@ import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; import { useAppSettings } from "../appSettings"; import { getAppLanguageDetails, type AppLanguage } from "../appLanguage"; import { isElectron } from "../env"; -import { APP_BASE_NAME, APP_VERSION } from "../branding"; +import { APP_VERSION } from "../branding"; +import { useTheme } from "../hooks/useTheme"; import { resolveServerHttpUrl } from "../lib/serverUrl"; import { cn, isMacPlatform, newCommandId } from "../lib/utils"; import { useStore } from "../store"; @@ -99,6 +100,7 @@ import { import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { useSidebarPreferencesStore } from "../sidebarPreferencesStore"; import { type SidebarArchiveFilterMode } from "../lib/threadOrdering"; +import { ProjectIconDialog } from "./ProjectIconDialog"; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; const THREAD_PREVIEW_LIMIT = 10; @@ -116,7 +118,7 @@ function getSidebarCopy(language: AppLanguage) { failedToRenameThread: "تغییر نام رشته انجام نشد", keepingOrphanedWorktree: "worktree یتیم حفظ شد", orphanedWorktreeKeptForSafety: (path: string) => - `CUT3 نتوانست بررسی کند که ${path} تغییرات ثبت نشده دارد یا نه، بنابراین worktree برای ایمنی حفظ می شود.`, + `Rowl نتوانست بررسی کند که ${path} تغییرات ثبت نشده دارد یا نه، بنابراین worktree برای ایمنی حفظ می شود.`, orphanedWorktreeWithChangesPrompt: (path: string) => [ "این رشته تنها رشته متصل به این worktree است:", @@ -216,7 +218,7 @@ function getSidebarCopy(language: AppLanguage) { failedToRenameThread: "Failed to rename thread", keepingOrphanedWorktree: "Keeping orphaned worktree", orphanedWorktreeKeptForSafety: (path: string) => - `CUT3 could not verify whether ${path} has uncommitted changes, so the worktree will be kept for safety.`, + `Rowl could not verify whether ${path} has uncommitted changes, so the worktree will be kept for safety.`, orphanedWorktreeWithChangesPrompt: (path: string) => [ "This thread is the only one linked to this worktree:", @@ -406,13 +408,16 @@ function localizeThreadStatusLabel(label: string, language: AppLanguage): string } function BrandMark() { - return ; + const { resolvedTheme } = useTheme(); + const src = resolvedTheme === "dark" ? "/rowl-logo-dark.png" : "/rowl-logo-light.png"; + return ; } -function ProjectFavicon({ cwd }: { cwd: string }) { +function ProjectFavicon({ cwd, refreshKey }: { cwd: string; refreshKey?: number }) { const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading"); + const key = refreshKey ?? 0; - const src = resolveServerHttpUrl(`/api/project-favicon?cwd=${encodeURIComponent(cwd)}`); + const src = resolveServerHttpUrl(`/api/project-favicon?cwd=${encodeURIComponent(cwd)}&k=${key}`); if (status === "error") { return ; @@ -420,6 +425,7 @@ function ProjectFavicon({ cwd }: { cwd: string }) { return ( (null); const [renamingThreadId, setRenamingThreadId] = useState(null); const [renamingTitle, setRenamingTitle] = useState(""); + const [iconDialogState, setIconDialogState] = useState<{ + open: boolean; + projectId: ProjectId | null; + projectName: string; + projectCwd: string; + }>({ open: false, projectId: null, projectName: "", projectCwd: "" }); + const [projectIconRefreshKeys, setProjectIconRefreshKeys] = useState>({}); const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< ReadonlySet >(() => new Set()); @@ -1108,6 +1121,7 @@ export default function Sidebar() { id: projectArchived ? "unarchive" : "archive", label: projectArchived ? sidebarCopy.unarchiveProject : sidebarCopy.archiveProject, }, + { id: "icon", label: "Project icon" }, { id: "delete", label: sidebarCopy.removeProject, destructive: true }, ], position, @@ -1120,6 +1134,15 @@ export default function Sidebar() { setProjectArchived(projectId, clicked === "archive"); return; } + if (clicked === "icon") { + setIconDialogState({ + open: true, + projectId, + projectName: project.name, + projectCwd: project.cwd, + }); + return; + } if (clicked !== "delete") return; const projectThreads = threads.filter((thread) => thread.projectId === projectId); @@ -1426,9 +1449,6 @@ export default function Sidebar() { render={
- - {APP_BASE_NAME} -
} /> @@ -1712,7 +1732,10 @@ export default function Sidebar() { project.expanded ? "rotate-90" : "" }`} /> - + {project.name} @@ -2040,6 +2063,39 @@ export default function Sidebar() { + + setIconDialogState((prev) => ({ ...prev, open }))} + projectId={iconDialogState.projectId ?? ""} + projectName={iconDialogState.projectName} + projectCwd={iconDialogState.projectCwd} + onSave={async (iconDataUrl) => { + const api = readNativeApi(); + if (!api || !iconDialogState.projectId) return; + await api.projects.writeFile({ + cwd: iconDialogState.projectCwd, + relativePath: ".rowl/icon.png", + contents: iconDataUrl, + }); + setProjectIconRefreshKeys((prev) => ({ + ...prev, + [iconDialogState.projectId!]: Date.now(), + })); + }} + onRemove={async () => { + const api = readNativeApi(); + if (!api || !iconDialogState.projectId) return; + await api.projects.deleteFile({ + cwd: iconDialogState.projectCwd, + relativePath: ".rowl/icon.png", + }); + setProjectIconRefreshKeys((prev) => ({ + ...prev, + [iconDialogState.projectId!]: Date.now(), + })); + }} + /> ); } diff --git a/apps/web/src/components/ThreadNewButton.browser.tsx b/apps/web/src/components/ThreadNewButton.browser.tsx index b3792f78e2f..6dcd0680c2e 100644 --- a/apps/web/src/components/ThreadNewButton.browser.tsx +++ b/apps/web/src/components/ThreadNewButton.browser.tsx @@ -41,7 +41,7 @@ const wsLink = ws.link(/ws(s)?:\/\/.*/); function createBaseServerConfig(): ServerConfig { return { cwd: "/repo/project", - keybindingsConfigPath: "/repo/project/.cut3-keybindings.json", + keybindingsConfigPath: "/repo/project/.rowl-keybindings.json", keybindings: [], issues: [], providers: [ @@ -77,6 +77,7 @@ function createSnapshot(): OrchestrationReadModel { id: THREAD_ID, projectId: PROJECT_ID, title: "Thread new button test", + goal: null, model: "gpt-5", interactionMode: "default", runtimeMode: "full-access", diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index 215f8326b80..2ee26958e2a 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -13,6 +13,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScr import { Toggle } from "../ui/toggle"; import { SidebarTrigger } from "../ui/sidebar"; import { OpenInPicker } from "./OpenInPicker"; +import RightSidebarToggle from "../RightSidebarToggle"; interface ChatHeaderProps { activeThreadId: ThreadId; @@ -32,6 +33,7 @@ interface ChatHeaderProps { onUpdateProjectScript: (scriptId: string, input: NewProjectScriptInput) => Promise; onDeleteProjectScript: (scriptId: string) => Promise; onToggleDiff: () => void; + onToggleRightSidebar?: () => void; } export const ChatHeader = memo(function ChatHeader({ @@ -52,6 +54,7 @@ export const ChatHeader = memo(function ChatHeader({ onUpdateProjectScript, onDeleteProjectScript, onToggleDiff, + onToggleRightSidebar, }: ChatHeaderProps) { return (
@@ -118,6 +121,7 @@ export const ChatHeader = memo(function ChatHeader({ : "Toggle diff panel"} +
); diff --git a/apps/web/src/components/chat/ComposerSkillPicker.tsx b/apps/web/src/components/chat/ComposerSkillPicker.tsx index db3317110dd..842f5488465 100644 --- a/apps/web/src/components/chat/ComposerSkillPicker.tsx +++ b/apps/web/src/components/chat/ComposerSkillPicker.tsx @@ -44,7 +44,7 @@ export const ComposerSkillPicker = memo(function ComposerSkillPicker(props: {
{props.skills.length === 0 ? (
- No skills found in .cut3/skills. + No skills found in .rowl/skills.
) : (
diff --git a/apps/web/src/components/chat/EmptyChatOnboarding.tsx b/apps/web/src/components/chat/EmptyChatOnboarding.tsx index 119db1d2928..9c431913317 100644 --- a/apps/web/src/components/chat/EmptyChatOnboarding.tsx +++ b/apps/web/src/components/chat/EmptyChatOnboarding.tsx @@ -13,18 +13,18 @@ function getEmptyChatOnboardingCopy(language: "en" | "fa") { if (language === "fa") { return { loadingTitle: "در حال آماده سازی پروژه ها...", - loadingDescription: "CUT3 در حال همگام سازی فضای کاری محلی شما است.", + loadingDescription: "Rowl در حال همگام سازی فضای کاری محلی شما است.", noProjectsTitle: "اولین پروژه خود را اضافه کنید", noProjectsDescription: - "CUT3 به یک پوشه پروژه نیاز دارد تا بتواند thread ها را ایجاد کند، دستورهای محلی را کشف کند، و AGENTS.md و skill های فضای کاری را بخواند.", + "Rowl به یک پوشه پروژه نیاز دارد تا بتواند thread ها را ایجاد کند، دستورهای محلی را کشف کند، و AGENTS.md و skill های فضای کاری را بخواند.", browse: "مرور پوشه", addProject: "افزودن پروژه", pathLabel: "مسیر پروژه", pathPlaceholder: "/path/to/project", - nextStepHint: "بعد از افزودن پروژه، CUT3 فوراً اولین thread پیش نویس را برای شما باز می کند.", + nextStepHint: "بعد از افزودن پروژه، Rowl فوراً اولین thread پیش نویس را برای شما باز می کند.", existingProjectsTitle: "یک thread جدید شروع کنید", existingProjectsDescription: (count: number) => - `${count} پروژه از قبل در CUT3 موجود است. می توانید یک thread جدید بسازید یا از سایدبار یک thread قبلی را ادامه دهید.`, + `${count} پروژه از قبل در Rowl موجود است. می توانید یک thread جدید بسازید یا از سایدبار یک thread قبلی را ادامه دهید.`, createThread: "ایجاد thread جدید", sidebarHint: "یا یک thread موجود را از سایدبار انتخاب کنید.", }; @@ -32,19 +32,19 @@ function getEmptyChatOnboardingCopy(language: "en" | "fa") { return { loadingTitle: "Preparing your projects...", - loadingDescription: "CUT3 is syncing the local workspace state.", + loadingDescription: "Rowl is syncing the local workspace state.", noProjectsTitle: "Add your first project", noProjectsDescription: - "CUT3 needs a project folder before it can create threads, discover repo-local commands, and read workspace AGENTS.md and skills.", + "Rowl needs a project folder before it can create threads, discover repo-local commands, and read workspace AGENTS.md and skills.", browse: "Browse for folder", addProject: "Add project", pathLabel: "Project path", pathPlaceholder: "/path/to/project", nextStepHint: - "After you add a project, CUT3 immediately opens your first draft thread so you can start working right away.", + "After you add a project, Rowl immediately opens your first draft thread so you can start working right away.", existingProjectsTitle: "Start a new thread", existingProjectsDescription: (count: number) => - `${count} project${count === 1 ? " is" : "s are"} already available in CUT3. Start a new thread or resume one from the sidebar.`, + `${count} project${count === 1 ? " is" : "s are"} already available in Rowl. Start a new thread or resume one from the sidebar.`, createThread: "Create new thread", sidebarHint: "Or pick an existing thread from the sidebar.", }; diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index fad31b39019..43bd0515d45 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -34,6 +34,7 @@ import { BarChart3Icon, BotIcon, ChevronDownIcon, + ChevronRightIcon, CheckIcon, PlusIcon, SearchIcon, @@ -42,11 +43,13 @@ import { BrainCircuitIcon, EyeIcon, SparklesIcon, + ChevronsUpDownIcon, } from "lucide-react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Badge } from "../ui/badge"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; +import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "../ui/collapsible"; import { ClaudeAI, CursorIcon, @@ -285,6 +288,8 @@ function getChatPickerCopy(language: AppLanguage) { recent: "اخیر", locked: "قفل شده", current: "فعلی", + default: "پیش‌فرض", + setAsDefault: "تنظیم به عنوان پیش‌فرض", }; } return { @@ -304,6 +309,8 @@ function getChatPickerCopy(language: AppLanguage) { recent: "Recent", locked: "Locked", current: "Current", + default: "Default", + setAsDefault: "Set as default", }; } @@ -318,14 +325,18 @@ const PickerModelRow = memo(function PickerModelRow(props: { isSelected: boolean; isFavorite: boolean; isRecent: boolean; + isDefault: boolean; favoriteLabel: string; recentLabel: string; + defaultLabel: string; + setAsDefaultLabel: string; isDisabledByProviderLock: boolean; disabled: boolean; serviceTierSetting: AppServiceTier; openRouterContextLengthsBySlug: ReadonlyMap; opencodeContextLengthsBySlug: ReadonlyMap; onSelect: () => void; + onSetAsDefault: () => void; }) { const displayParts = getModelPickerOptionDisplayParts(props.modelOption); const contextLabel = getModelOptionContextLabel( @@ -393,6 +404,26 @@ const PickerModelRow = memo(function PickerModelRow(props: { {props.recentLabel} ) : null} + {props.isDefault ? ( + + {props.defaultLabel} + + ) : !props.isDefault && !props.isDisabledByProviderLock && !props.disabled ? ( + + ) : null} {props.modelOption.supportsReasoning ? ( ; serviceTierSetting: AppServiceTier; hasHiddenModels: boolean; + hiddenProviders: ReadonlyArray; favoriteModelsByProvider: Record>; recentModelsByProvider: Record>; modelLabelOverride?: string; compact?: boolean; disabled?: boolean; + projectDefaultModel: string | null; onOpenProviderSetup: () => void; onOpenManageModels: () => void; onOpenUsageDashboard: () => void; onProviderModelChange: (provider: AvailableProviderPickerKind, model: ModelSlug) => void; + onSetAsDefault: (provider: AvailableProviderPickerKind, model: ModelSlug) => void; }) { const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(""); @@ -476,7 +510,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { selectedProviderOptions.find((option) => option.slug === props.model)?.name ?? getModelDisplayName(props.model, props.provider); const selectedProviderLabel = - AVAILABLE_PROVIDER_OPTIONS.find((option) => option.value === props.providerPickerKind)?.label ?? + PROVIDER_OPTIONS.find((option) => option.value === props.providerPickerKind)?.label ?? props.providerPickerKind; const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[props.providerPickerKind]; @@ -521,10 +555,15 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { ); const normalizedQuery = query.trim().toLowerCase(); + const visibleAvailableOptions = useMemo( + () => + AVAILABLE_PROVIDER_OPTIONS.filter((option) => !props.hiddenProviders.includes(option.value)), + [props.hiddenProviders], + ); const providerSections = useMemo( () => buildPickerProviderSections({ - availableOptions: AVAILABLE_PROVIDER_OPTIONS, + availableOptions: visibleAvailableOptions, visibleModelOptionsByProvider: props.visibleModelOptionsByProvider, openRouterModelOptions: props.openRouterModelOptions, opencodeModelOptions: props.opencodeModelOptions, @@ -535,6 +574,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { }), [ normalizedQuery, + visibleAvailableOptions, props.openRouterModelOptions, props.opencodeModelOptions, props.visibleModelOptionsByProvider, @@ -544,26 +584,71 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { ], ); - const unavailableOptions = useMemo(() => { - const placeholderOptions = [ - ...UNAVAILABLE_PROVIDER_OPTIONS.map((option) => ({ - id: option.value, - label: option.label, - icon: PROVIDER_ICON_BY_PROVIDER[option.value], - })), - ...COMING_SOON_PROVIDER_OPTIONS.map((option) => ({ - id: option.id, - label: option.label, - icon: option.icon, - })), - ]; - if (!normalizedQuery) { - return placeholderOptions; + // Collapsed state management + const STORAGE_KEY = "rowl:model-picker:collapsed-sections"; + const [collapsedSections, setCollapsedSections] = useState>( + () => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + return new Set(JSON.parse(stored) as AvailableProviderPickerKind[]); + } + } catch { + // ignore parse errors + } + return new Set(); + }, + ); + + // Auto-expand selected provider when popover opens + useEffect(() => { + if (isOpen && !normalizedQuery) { + setCollapsedSections((prev) => { + if (prev.has(props.providerPickerKind)) { + const next = new Set(prev); + next.delete(props.providerPickerKind); + return next; + } + return prev; + }); } - return placeholderOptions.filter((option) => - option.label.toLowerCase().includes(normalizedQuery), - ); - }, [normalizedQuery]); + }, [isOpen, props.providerPickerKind, normalizedQuery]); + + // Persist collapsed state + useEffect(() => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify([...collapsedSections])); + } catch { + // ignore storage errors + } + }, [collapsedSections]); + + const toggleSection = useCallback((value: AvailableProviderPickerKind) => { + setCollapsedSections((prev) => { + const next = new Set(prev); + if (next.has(value)) { + next.delete(value); + } else { + next.add(value); + } + return next; + }); + }, []); + + const expandAll = useCallback(() => { + setCollapsedSections(new Set()); + }, []); + + const collapseAll = useCallback(() => { + setCollapsedSections(new Set(providerSections.map((s) => s.option.value))); + }, [providerSections]); + + const allCollapsed = + providerSections.length > 0 && + providerSections.every((s) => collapsedSections.has(s.option.value)); + const allExpanded = + providerSections.length > 0 && + providerSections.every((s) => !collapsedSections.has(s.option.value)); // Total model count across visible sections const totalVisibleModels = providerSections.reduce((sum, s) => sum + s.modelOptions.length, 0); @@ -634,12 +719,38 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
{/* Quick stat */} {!normalizedQuery ? ( -
- - - {providerSections.length} provider{providerSections.length === 1 ? "" : "s"} ·{" "} - {totalVisibleModels} model{totalVisibleModels === 1 ? "" : "s"} available - +
+
+ + + {providerSections.length} provider{providerSections.length === 1 ? "" : "s"} ·{" "} + {totalVisibleModels} model{totalVisibleModels === 1 ? "" : "s"} available + +
+ {providerSections.length > 1 ? ( +
+ +
+ ) : null}
) : null}
@@ -692,8 +803,9 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { }) : null; + const isCollapsed = collapsedSections.has(section.option.value); return ( -
toggleSection(section.option.value)} > {/* Provider section header */} -
+ {section.option.label} @@ -736,115 +856,118 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { {copy.models(section.modelOptions.length)} -
- - {/* Context window & copilot usage */} - {providerContextSummary ? ( -
{providerContextSummary}
- ) : null} - - {section.option.value === "copilot" && isCurrentProvider ? ( -
- {renderCopilotUsageSummary( - copilotUsageQuery.data ?? null, - copilotUsageQuery.isLoading, - props.language, - )} -
- ) : null} - - {/* Model rows grouped by family */} -
- {section.families.map((family, familyIdx) => ( -
- {/* Family label (only when multiple families) */} - {section.families.length > 1 && family.label ? ( -
0 && "border-t border-border/30", - )} - > - - {family.label} - - - - {family.models.length} - -
- ) : null} - {family.models.map((modelOption) => { - const isSelected = - section.option.value === props.providerPickerKind && - modelOption.slug === props.model; - const providerFavorites = - props.favoriteModelsByProvider[section.backingProvider]; - const providerRecents = - props.recentModelsByProvider[section.backingProvider]; - return ( - { - handleModelChange( - section.option.value, - modelOption.slug, - section.modelOptions, - section.isDisabledByProviderLock, - ); - }} - /> - ); - })} + + + + {/* Context window & copilot usage */} + {providerContextSummary ? ( +
{providerContextSummary}
+ ) : null} + + {section.option.value === "copilot" && isCurrentProvider ? ( +
+ {renderCopilotUsageSummary( + copilotUsageQuery.data ?? null, + copilotUsageQuery.isLoading, + props.language, + )}
- ))} -
-
+ ) : null} + + {/* Model rows grouped by family */} +
+ {section.families.map((family, familyIdx) => ( +
+ {/* Family label (only when multiple families) */} + {section.families.length > 1 && family.label ? ( +
0 && "border-t border-border/30", + )} + > + + {family.label} + + + + {family.models.length} + +
+ ) : null} + {family.models.map((modelOption) => { + const isSelected = + section.option.value === props.providerPickerKind && + modelOption.slug === props.model; + const providerFavorites = + props.favoriteModelsByProvider[section.backingProvider]; + const providerRecents = + props.recentModelsByProvider[section.backingProvider]; + return ( + { + handleModelChange( + section.option.value, + modelOption.slug, + section.modelOptions, + section.isDisabledByProviderLock, + ); + }} + onSetAsDefault={() => { + props.onSetAsDefault(section.option.value, modelOption.slug); + }} + /> + ); + })} +
+ ))} +
+ + ); }) )} - - {/* Coming soon / unavailable */} - {unavailableOptions.length > 0 ? ( -
-
- {copy.comingSoon} -
-
- {unavailableOptions.map((option) => { - const OptionIcon = option.icon; - return ( - - - {option.label} - - ); - })} -
-
- ) : null}
{/* Footer toolbar */}
-

- {props.hasHiddenModels ? copy.hiddenModelsHint : copy.pickModelHint} -

+
+

+ {props.hasHiddenModels ? copy.hiddenModelsHint : copy.pickModelHint} +

+ {props.model !== props.projectDefaultModel && ( + + )} +
+
+ ); +}); diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index e5eceeafde4..7d24b61fe63 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -54,17 +54,17 @@ export function getArm64IntelBuildWarningDescription( const action = resolveDesktopUpdateButtonAction(state); if (action === "download") { return language === "fa" - ? "این مک Apple Silicon دارد، اما CUT3 هنوز نسخه اینتل را با Rosetta اجرا می کند. برای رفتن به نسخه بومی Apple Silicon، به روزرسانی موجود را دانلود کنید." - : "This Mac has Apple Silicon, but CUT3 is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build."; + ? "این مک Apple Silicon دارد، اما Rowl هنوز نسخه اینتل را با Rosetta اجرا می کند. برای رفتن به نسخه بومی Apple Silicon، به روزرسانی موجود را دانلود کنید." + : "This Mac has Apple Silicon, but Rowl is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build."; } if (action === "install") { return language === "fa" - ? "این مک Apple Silicon دارد، اما CUT3 هنوز نسخه اینتل را با Rosetta اجرا می کند. برای نصب نسخه دانلودشده Apple Silicon دوباره راه اندازی کنید." - : "This Mac has Apple Silicon, but CUT3 is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build."; + ? "این مک Apple Silicon دارد، اما Rowl هنوز نسخه اینتل را با Rosetta اجرا می کند. برای نصب نسخه دانلودشده Apple Silicon دوباره راه اندازی کنید." + : "This Mac has Apple Silicon, but Rowl is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build."; } return language === "fa" - ? "این مک Apple Silicon دارد، اما CUT3 هنوز نسخه اینتل را با Rosetta اجرا می کند. به روزرسانی بعدی برنامه آن را با نسخه بومی Apple Silicon جایگزین می کند." - : "This Mac has Apple Silicon, but CUT3 is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build."; + ? "این مک Apple Silicon دارد، اما Rowl هنوز نسخه اینتل را با Rosetta اجرا می کند. به روزرسانی بعدی برنامه آن را با نسخه بومی Apple Silicon جایگزین می کند." + : "This Mac has Apple Silicon, but Rowl is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build."; } export function getDesktopUpdateButtonTooltip( diff --git a/apps/web/src/components/right-sidebar/RightSidebar.tsx b/apps/web/src/components/right-sidebar/RightSidebar.tsx new file mode 100644 index 00000000000..7a0dd4ddbbb --- /dev/null +++ b/apps/web/src/components/right-sidebar/RightSidebar.tsx @@ -0,0 +1,134 @@ +import { memo, useState, useCallback } from "react"; +import { + PanelRightCloseIcon, + MessageSquareIcon, + ListIcon, + LayoutGridIcon, + TargetIcon, + DatabaseIcon, +} from "lucide-react"; +import { cn } from "~/lib/utils"; +import { useRightSidebar } from "./RightSidebarContext"; + +export { RightSidebarProvider } from "./RightSidebarContext"; + +export type RightSidebarTab = "pm-chat" | "threads" | "features" | "goals" | "context"; + +interface TabConfig { + id: RightSidebarTab; + label: string; + icon: React.ReactNode; +} + +const TABS: TabConfig[] = [ + { id: "pm-chat", label: "PM Chat", icon: }, + { id: "threads", label: "Threads", icon: }, + { id: "features", label: "Features", icon: }, + { id: "goals", label: "Goals", icon: }, + { id: "context", label: "Context", icon: }, +]; + +function renderTabContent(activeTab: RightSidebarTab) { + return ( +
+ {activeTab} tab coming soon +
+ ); +} + +const RightSidebar = memo(function RightSidebar({ className }: { className?: string }) { + const { rightSidebarExpanded, setRightSidebarExpanded } = useRightSidebar(); + const [activeTab, setActiveTab] = useState("pm-chat"); + + const toggleExpanded = useCallback(() => { + setRightSidebarExpanded(!rightSidebarExpanded); + }, [rightSidebarExpanded, setRightSidebarExpanded]); + + const renderCollapsed = () => ( +
+
+ {TABS.map((tab) => ( + + ))} +
+
+ ); + + const renderExpanded = () => ( +
+
+
+ {TABS.map( + (tab) => + activeTab === tab.id && ( + + {tab.icon} + {tab.label} + + ), + )} +
+ +
+ +
+ {TABS.map((tab) => ( + + ))} +
+ +
{renderTabContent(activeTab)}
+
+ ); + + return ( +
+ {rightSidebarExpanded ? renderExpanded() : renderCollapsed()} +
+ ); +}); + +export default RightSidebar; diff --git a/apps/web/src/components/right-sidebar/RightSidebarContext.tsx b/apps/web/src/components/right-sidebar/RightSidebarContext.tsx new file mode 100644 index 00000000000..9ff6ef39370 --- /dev/null +++ b/apps/web/src/components/right-sidebar/RightSidebarContext.tsx @@ -0,0 +1,36 @@ +import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react"; + +interface RightSidebarContextValue { + rightSidebarExpanded: boolean; + setRightSidebarExpanded: (expanded: boolean) => void; + toggleRightSidebar: () => void; +} + +const RightSidebarContext = createContext(null); + +interface RightSidebarProviderProps { + children: ReactNode; +} + +export function RightSidebarProvider({ children }: RightSidebarProviderProps) { + const [rightSidebarExpanded, setRightSidebarExpanded] = useState(false); + + const toggleRightSidebar = useCallback(() => { + setRightSidebarExpanded((prev) => !prev); + }, []); + + const value = useMemo( + () => ({ rightSidebarExpanded, setRightSidebarExpanded, toggleRightSidebar }), + [rightSidebarExpanded, toggleRightSidebar], + ); + + return {children}; +} + +export function useRightSidebar(): RightSidebarContextValue { + const context = useContext(RightSidebarContext); + if (!context) { + throw new Error("useRightSidebar must be used within a RightSidebarProvider"); + } + return context; +} diff --git a/apps/web/src/components/settings/PermissionPoliciesSection.tsx b/apps/web/src/components/settings/PermissionPoliciesSection.tsx index 42bb94e3fde..28df1f16de6 100644 --- a/apps/web/src/components/settings/PermissionPoliciesSection.tsx +++ b/apps/web/src/components/settings/PermissionPoliciesSection.tsx @@ -136,7 +136,7 @@ export function PermissionPoliciesSection({
Current runtime approvals expose command, file-read, file-change, raw request type, and truncated detail text. If a provider starts emitting a new request type later, you can - target it with Request type terms even before CUT3 adds a dedicated label for it. + target it with Request type terms even before Rowl adds a dedicated label for it.
diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index b83b3fe1842..f5d604cd364 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -14,7 +14,7 @@ import { Debouncer } from "@tanstack/react-pacer"; import { create } from "zustand"; import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; -export const COMPOSER_DRAFT_STORAGE_KEY = "cut3:composer-drafts:v1"; +export const COMPOSER_DRAFT_STORAGE_KEY = "rowl:composer-drafts:v1"; export type DraftThreadEnvMode = "local" | "worktree"; const COMPOSER_PERSIST_DEBOUNCE_MS = 300; diff --git a/apps/web/src/editorPreferences.ts b/apps/web/src/editorPreferences.ts index 5054b1c990a..be065a81693 100644 --- a/apps/web/src/editorPreferences.ts +++ b/apps/web/src/editorPreferences.ts @@ -2,7 +2,7 @@ import { EDITORS, EditorId, NativeApi } from "@t3tools/contracts"; import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "./hooks/useLocalStorage"; import { useMemo } from "react"; -const LAST_EDITOR_KEY = "cut3:last-editor"; +const LAST_EDITOR_KEY = "rowl:last-editor"; export function usePreferredEditor(availableEditors: ReadonlyArray) { const [lastEditor, setLastEditor] = useLocalStorage(LAST_EDITOR_KEY, null, EditorId); diff --git a/apps/web/src/hooks/useLocalStorage.ts b/apps/web/src/hooks/useLocalStorage.ts index 3c63ed0e375..de230838b18 100644 --- a/apps/web/src/hooks/useLocalStorage.ts +++ b/apps/web/src/hooks/useLocalStorage.ts @@ -39,7 +39,7 @@ export const removeLocalStorageItem = (key: string) => { isomorphicLocalStorage.removeItem(key); }; -const LOCAL_STORAGE_CHANGE_EVENT = "cut3:local_storage_change"; +const LOCAL_STORAGE_CHANGE_EVENT = "rowl:local_storage_change"; interface LocalStorageChangeDetail { key: string; diff --git a/apps/web/src/hooks/useProjectCreationActions.ts b/apps/web/src/hooks/useProjectCreationActions.ts index 0460ee3fb4f..3866fb3d473 100644 --- a/apps/web/src/hooks/useProjectCreationActions.ts +++ b/apps/web/src/hooks/useProjectCreationActions.ts @@ -2,6 +2,7 @@ import { DEFAULT_MODEL_BY_PROVIDER, type ProjectId, type ThreadId } from "@t3too import { useNavigate } from "@tanstack/react-router"; import { useCallback, useMemo, useState } from "react"; +import { APP_DISPLAY_NAME } from "../branding"; import { useAppSettings } from "../appSettings"; import { useNewThreadActions } from "./useNewThread"; import { compareThreadsByRecency } from "../lib/threadOrdering"; @@ -97,8 +98,8 @@ export function useProjectCreationActions() { } catch (error) { const message = error instanceof Error && error.message.trim().length > 0 - ? `Project was added, but CUT3 could not open its first draft thread: ${error.message}` - : "Project was added, but CUT3 could not open its first draft thread."; + ? `Project was added, but ${APP_DISPLAY_NAME} could not open its first draft thread: ${error.message}` + : `Project was added, but ${APP_DISPLAY_NAME} could not open its first draft thread.`; setAddProjectError(message); return { ok: false, message }; } diff --git a/apps/web/src/hooks/useTheme.ts b/apps/web/src/hooks/useTheme.ts index af95459635a..59011d554ee 100644 --- a/apps/web/src/hooks/useTheme.ts +++ b/apps/web/src/hooks/useTheme.ts @@ -20,7 +20,7 @@ type ThemeSnapshot = { customThemeId: CustomThemeId; }; -const STORAGE_KEY = "cut3:theme"; +const STORAGE_KEY = "rowl:theme"; const MEDIA_QUERY = "(prefers-color-scheme: dark)"; let listeners: Array<() => void> = []; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 6e09e1adc4f..53584d05577 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -414,9 +414,9 @@ --sidebar-accent: var(--accent); --sidebar-accent-foreground: var(--accent-foreground); --sidebar-ring: var(--ring); - --cut3-sidebar-backdrop-filter: none; - --cut3-sidebar-surface: var(--sidebar); - --cut3-sidebar-surface-solid: var(--sidebar); + --rowl-sidebar-backdrop-filter: none; + --rowl-sidebar-surface: var(--sidebar); + --rowl-sidebar-surface-solid: var(--sidebar); @variant dark { color-scheme: dark; @@ -451,9 +451,9 @@ --sidebar-accent: var(--accent); --sidebar-accent-foreground: var(--accent-foreground); --sidebar-ring: var(--ring); - --cut3-sidebar-backdrop-filter: none; - --cut3-sidebar-surface: var(--sidebar); - --cut3-sidebar-surface-solid: var(--sidebar); + --rowl-sidebar-backdrop-filter: none; + --rowl-sidebar-surface: var(--sidebar); + --rowl-sidebar-surface-solid: var(--sidebar); } } @@ -856,12 +856,12 @@ button { [data-slot="sidebar-inner"], [data-slot="sidebar"][data-mobile="true"] { - backdrop-filter: var(--cut3-sidebar-backdrop-filter); + backdrop-filter: var(--rowl-sidebar-backdrop-filter); } html[data-sidebar-translucent="on"] [data-slot="sidebar-inner"], html[data-sidebar-translucent="on"] [data-slot="sidebar"][data-mobile="true"] { - background: var(--cut3-sidebar-surface) !important; + background: var(--rowl-sidebar-surface) !important; } @media (min-width: 48rem) { diff --git a/apps/web/src/lib/appearanceTheme.ts b/apps/web/src/lib/appearanceTheme.ts index 11009f5c631..d65e1784740 100644 --- a/apps/web/src/lib/appearanceTheme.ts +++ b/apps/web/src/lib/appearanceTheme.ts @@ -328,13 +328,13 @@ export function deriveAppearanceCssVariables( "--border": border, "--card": card, "--card-foreground": foreground, - "--cut3-sidebar-backdrop-filter": normalized.translucentSidebar + "--rowl-sidebar-backdrop-filter": normalized.translucentSidebar ? "blur(24px) saturate(1.12)" : "none", - "--cut3-sidebar-surface": normalized.translucentSidebar + "--rowl-sidebar-surface": normalized.translucentSidebar ? withAlpha(sidebarBase, appearance === "light" ? 0.78 : 0.72) : sidebarBase, - "--cut3-sidebar-surface-solid": sidebarBase, + "--rowl-sidebar-surface-solid": sidebarBase, "--foreground": foreground, "--font-code-snippet": normalized.codeFont, "--font-ui": normalized.uiFont, @@ -373,9 +373,9 @@ export function clearAppliedAppearanceCssVariables(): void { "--border", "--card", "--card-foreground", - "--cut3-sidebar-backdrop-filter", - "--cut3-sidebar-surface", - "--cut3-sidebar-surface-solid", + "--rowl-sidebar-backdrop-filter", + "--rowl-sidebar-surface", + "--rowl-sidebar-surface-solid", "--foreground", "--font-code-snippet", "--font-ui", @@ -426,16 +426,16 @@ export function applyGlobalAppearanceSettings(args: { normalizeFontStack(args.themeConfig.codeFont, DEFAULT_CODE_FONT), ); root.style.setProperty( - "--cut3-sidebar-backdrop-filter", + "--rowl-sidebar-backdrop-filter", args.themeConfig.translucentSidebar ? "blur(24px) saturate(1.12)" : "none", ); root.style.setProperty( - "--cut3-sidebar-surface", + "--rowl-sidebar-surface", args.themeConfig.translucentSidebar ? "color-mix(in srgb, var(--sidebar) 76%, transparent)" : "var(--sidebar)", ); - root.style.setProperty("--cut3-sidebar-surface-solid", "var(--sidebar)"); + root.style.setProperty("--rowl-sidebar-surface-solid", "var(--sidebar)"); if (args.customThemeEnabled) { return; diff --git a/apps/web/src/lib/chatBackgroundStorage.ts b/apps/web/src/lib/chatBackgroundStorage.ts index 676cfc3e5e5..3e112853ab8 100644 --- a/apps/web/src/lib/chatBackgroundStorage.ts +++ b/apps/web/src/lib/chatBackgroundStorage.ts @@ -1,4 +1,4 @@ -const DB_NAME = "cut3-assets"; +const DB_NAME = "rowl-assets"; const DB_VERSION = 1; const STORE_NAME = "chat-background-images"; diff --git a/apps/web/src/lib/customThemes.ts b/apps/web/src/lib/customThemes.ts index 2d07db1aa08..1783bdaf1a7 100644 --- a/apps/web/src/lib/customThemes.ts +++ b/apps/web/src/lib/customThemes.ts @@ -156,7 +156,7 @@ export const CUSTOM_THEME_OPTIONS = [ { id: "none", label: "Default theme", - description: "Use CUT3's built-in theme tokens.", + description: "Use Rowl's built-in theme tokens.", family: "default", appearance: null, }, diff --git a/apps/web/src/lib/openRouterModels.test.ts b/apps/web/src/lib/openRouterModels.test.ts index 34611191b3d..e92ac19382e 100644 --- a/apps/web/src/lib/openRouterModels.test.ts +++ b/apps/web/src/lib/openRouterModels.test.ts @@ -230,7 +230,7 @@ describe("openRouterModels", () => { it("reuses the last known-good catalog when the live fetch fails", async () => { const storage = new Map(); storage.set( - "cut3:openrouter-free-models-cache:v1", + "rowl:openrouter-free-models-cache:v1", JSON.stringify({ fetchedAt: "2026-03-27T10:00:00.000Z", models: [ diff --git a/apps/web/src/lib/openRouterModels.ts b/apps/web/src/lib/openRouterModels.ts index 5c6f8c6a8ef..54b15c980e7 100644 --- a/apps/web/src/lib/openRouterModels.ts +++ b/apps/web/src/lib/openRouterModels.ts @@ -30,7 +30,7 @@ export type OpenRouterFreeModelCatalog = readonly models: ReadonlyArray; }; -const OPENROUTER_FREE_MODEL_CACHE_STORAGE_KEY = "cut3:openrouter-free-models-cache:v1"; +const OPENROUTER_FREE_MODEL_CACHE_STORAGE_KEY = "rowl:openrouter-free-models-cache:v1"; export const OPENROUTER_FREE_ROUTER_OPTION: OpenRouterFreeModelOption = { slug: OPENROUTER_FREE_ROUTER_MODEL, diff --git a/apps/web/src/lib/threadOrdering.test.ts b/apps/web/src/lib/threadOrdering.test.ts index 40dc28da6dc..0111391790a 100644 --- a/apps/web/src/lib/threadOrdering.test.ts +++ b/apps/web/src/lib/threadOrdering.test.ts @@ -28,6 +28,7 @@ const thread = (overrides: Partial = {}): Thread => ({ codexThreadId: null, projectId: projectId("project-1"), title: "First thread", + goal: null, model: "gpt-5.4", runtimeMode: "full-access", interactionMode: "default", diff --git a/apps/web/src/notifications.ts b/apps/web/src/notifications.ts index 71a026f471f..645ad9006ab 100644 --- a/apps/web/src/notifications.ts +++ b/apps/web/src/notifications.ts @@ -12,11 +12,11 @@ const NOTIFICATION_AUTO_CLOSE_MS = 8_000; function getNotificationCopy(language: string) { if (language === "fa") { return { - title: "CUT3 — کار تمام شد", + title: "Rowl — کار تمام شد", }; } return { - title: "CUT3 — Task Complete", + title: "Rowl — Task Complete", }; } diff --git a/apps/web/src/projectCommandTemplates.test.ts b/apps/web/src/projectCommandTemplates.test.ts index 2782151cbb4..2b219de55e6 100644 --- a/apps/web/src/projectCommandTemplates.test.ts +++ b/apps/web/src/projectCommandTemplates.test.ts @@ -9,7 +9,7 @@ import { const DEPLOY_TEMPLATE: ProjectCommandTemplate = { name: "deploy", - relativePath: ".cut3/commands/deploy.md", + relativePath: ".rowl/commands/deploy.md", description: "Deploy the selected target", template: "Deploy $1 with notes: $ARGUMENTS", provider: "codex", diff --git a/apps/web/src/projectScripts.test.ts b/apps/web/src/projectScripts.test.ts index 2eb637887f5..97cb74fc04f 100644 --- a/apps/web/src/projectScripts.test.ts +++ b/apps/web/src/projectScripts.test.ts @@ -52,8 +52,8 @@ describe("projectScripts helpers", () => { }); expect(env).toMatchObject({ - CUT3_PROJECT_ROOT: "/repo", - CUT3_WORKTREE_PATH: "/repo/worktree-a", + ROWL_PROJECT_ROOT: "/repo", + ROWL_WORKTREE_PATH: "/repo/worktree-a", }); }); @@ -61,13 +61,13 @@ describe("projectScripts helpers", () => { const env = projectScriptRuntimeEnv({ project: { cwd: "/repo" }, extraEnv: { - CUT3_PROJECT_ROOT: "/custom-root", + ROWL_PROJECT_ROOT: "/custom-root", CUSTOM_FLAG: "1", }, }); - expect(env.CUT3_PROJECT_ROOT).toBe("/custom-root"); + expect(env.ROWL_PROJECT_ROOT).toBe("/custom-root"); expect(env.CUSTOM_FLAG).toBe("1"); - expect(env.CUT3_WORKTREE_PATH).toBeUndefined(); + expect(env.ROWL_WORKTREE_PATH).toBeUndefined(); }); }); diff --git a/apps/web/src/projectScripts.ts b/apps/web/src/projectScripts.ts index 577eab40624..315a9102aec 100644 --- a/apps/web/src/projectScripts.ts +++ b/apps/web/src/projectScripts.ts @@ -67,10 +67,10 @@ export function projectScriptRuntimeEnv( input: ProjectScriptRuntimeEnvInput, ): Record { const env: Record = { - CUT3_PROJECT_ROOT: input.project.cwd, + ROWL_PROJECT_ROOT: input.project.cwd, }; if (input.worktreePath) { - env.CUT3_WORKTREE_PATH = input.worktreePath; + env.ROWL_WORKTREE_PATH = input.worktreePath; } if (input.extraEnv) { return { ...env, ...input.extraEnv }; diff --git a/apps/web/src/providerStatus.ts b/apps/web/src/providerStatus.ts index f960c2a0ded..5677a5e0490 100644 --- a/apps/web/src/providerStatus.ts +++ b/apps/web/src/providerStatus.ts @@ -115,6 +115,12 @@ export function resolveVisibleProviderStatusForChat(input: { return null; } + // Only show provider status when the provider is actively in use + // (either as the selected provider or as the running session provider) + if (status.provider !== input.selectedProvider && status.provider !== input.sessionProvider) { + return null; + } + if (status.provider === "codex" && input.selectedModelUsesOpenRouter) { return status; } diff --git a/apps/web/src/pullRequestReference.test.ts b/apps/web/src/pullRequestReference.test.ts index 4364d7092b5..fc4aa0d6bce 100644 --- a/apps/web/src/pullRequestReference.test.ts +++ b/apps/web/src/pullRequestReference.test.ts @@ -4,8 +4,8 @@ import { parsePullRequestReference } from "./pullRequestReference"; describe("parsePullRequestReference", () => { it("accepts GitHub pull request URLs", () => { - expect(parsePullRequestReference("https://github.com/pingdotgg/cut3/pull/42")).toBe( - "https://github.com/pingdotgg/cut3/pull/42", + expect(parsePullRequestReference("https://github.com/pingdotgg/rowl/pull/42")).toBe( + "https://github.com/pingdotgg/rowl/pull/42", ); }); diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index f69b5f2a2e9..0ff3e5325b2 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -112,7 +112,7 @@ function getSettingsCopy(language: AppLanguage) { blurDescription: "برای نرم تر شدن والپیپرهای پرجزئیات پشت پیام ها، تاری را بیشتر کنید.", resetImageEffects: "بازنشانی افکت های تصویر", imageStorageNote: (sizeLabel: string) => - `CUT3 این تصویر را در تنظیمات محلی همین دستگاه نگه می دارد. اندازه آن را حداکثر ${sizeLabel} نگه دارید.`, + `Rowl این تصویر را در تنظیمات محلی همین دستگاه نگه می دارد. اندازه آن را حداکثر ${sizeLabel} نگه دارید.`, chooseImageFile: "یک فایل تصویری انتخاب کنید.", imageTooLarge: (sizeLabel: string) => `یک تصویر حداکثر ${sizeLabel} انتخاب کنید تا به صورت محلی ذخیره شود.`, @@ -130,12 +130,12 @@ function getSettingsCopy(language: AppLanguage) { resetCodexOverrides: "بازنشانی بازنویسی های Codex", openRouterTitle: "OpenRouter", openRouterDescription: - "CUT3، OpenRouter را به صورت یک بخش مستقل در رابط نشان می دهد و این نشست ها را از پشت صحنه از طریق Codex اجرا می کند؛ بنابراین می توانید از openrouter/free یا مدل های ذخیره شده :free بدون تغییر پیکربندی عادی Codex استفاده کنید.", + "Rowl، OpenRouter را به صورت یک بخش مستقل در رابط نشان می دهد و این نشست ها را از پشت صحنه از طریق Codex اجرا می کند؛ بنابراین می توانید از openrouter/free یا مدل های ذخیره شده :free بدون تغییر پیکربندی عادی Codex استفاده کنید.", openRouterApiKey: "کلید API OpenRouter", openRouterKeyDescription: (electron: boolean) => electron - ? "فقط برای مدل های Codex که از OpenRouter عبور می کنند لازم است. CUT3 آن را در نشست دسکتاپ نگه می دارد و در صورت وجود ذخیره سازی امن، در مخزن اعتبار سیستم عامل ذخیره می کند. برای مجموعه مدل های رایگان فعلی از openrouter/free استفاده کنید یا در ادامه اسلاگ های :free مشخص را اضافه کنید." - : "فقط برای مدل های Codex که از OpenRouter عبور می کنند لازم است. CUT3 آن را فقط در حافظه نشست فعلی مرورگر نگه می دارد. برای مجموعه مدل های رایگان فعلی از openrouter/free استفاده کنید یا در ادامه اسلاگ های :free مشخص را اضافه کنید.", + ? "فقط برای مدل های Codex که از OpenRouter عبور می کنند لازم است. Rowl آن را در نشست دسکتاپ نگه می دارد و در صورت وجود ذخیره سازی امن، در مخزن اعتبار سیستم عامل ذخیره می کند. برای مجموعه مدل های رایگان فعلی از openrouter/free استفاده کنید یا در ادامه اسلاگ های :free مشخص را اضافه کنید." + : "فقط برای مدل های Codex که از OpenRouter عبور می کنند لازم است. Rowl آن را فقط در حافظه نشست فعلی مرورگر نگه می دارد. برای مجموعه مدل های رایگان فعلی از openrouter/free استفاده کنید یا در ادامه اسلاگ های :free مشخص را اضافه کنید.", openRouterConfigured: "کلید OpenRouter برای نشست های جدید Codex تنظیم شده است.", openRouterMissing: "برای استفاده از مدل های Codex مبتنی بر OpenRouter یک کلید اضافه کنید.", resetOpenRouterKey: "بازنشانی کلید OpenRouter", @@ -147,7 +147,7 @@ function getSettingsCopy(language: AppLanguage) { resetCopilotOverrides: "بازنشانی بازنویسی های Copilot", opencodeTitle: "OpenCode", opencodeDescription: - "برای نشست های جدید OpenCode اعمال می شود و به CUT3 اجازه می دهد از نصب غیرپیش فرض opencode استفاده کند. اعتبارنامه ها را با opencode auth login و opencode auth logout مدیریت کنید، و اگر پیکربندی OpenCode شما به OPENROUTER_API_KEY نیاز دارد کلید OpenRouter را در بخش بالایی CUT3 وارد کنید.", + "برای نشست های جدید OpenCode اعمال می شود و به Rowl اجازه می دهد از نصب غیرپیش فرض opencode استفاده کند. اعتبارنامه ها را با opencode auth login و opencode auth logout مدیریت کنید، و اگر پیکربندی OpenCode شما به OPENROUTER_API_KEY نیاز دارد کلید OpenRouter را در بخش بالایی Rowl وارد کنید.", opencodeBinaryPath: "مسیر باینری", leaveBlankOpencode: "برای استفاده از opencode از PATH این کادر را خالی بگذارید.", resetOpencodeOverrides: "بازنشانی بازنویسی ها", @@ -159,12 +159,12 @@ function getSettingsCopy(language: AppLanguage) { kimiApiKey: "کلید API Kimi", kimiApiDescription: (electron: boolean) => electron - ? "اگر می خواهید CUT3 نشست های Kimi را مستقیم شروع کند، این کلید را از Kimi Code Console بسازید. CUT3 آن را در نشست دسکتاپ نگه می دارد و در صورت وجود ذخیره سازی امن، در مخزن اعتبار سیستم عامل ذخیره می کند. اگر ترجیح می دهید از ورود محلی CLI استفاده کنید، این فیلد را خالی بگذارید و با kimi login یا /login وارد شوید." - : "اگر می خواهید CUT3 نشست های Kimi را مستقیم شروع کند، این کلید را از Kimi Code Console بسازید. CUT3 آن را فقط در حافظه نشست فعلی مرورگر نگه می دارد. اگر ترجیح می دهید از ورود محلی CLI استفاده کنید، این فیلد را خالی بگذارید و با kimi login یا /login وارد شوید.", + ? "اگر می خواهید Rowl نشست های Kimi را مستقیم شروع کند، این کلید را از Kimi Code Console بسازید. Rowl آن را در نشست دسکتاپ نگه می دارد و در صورت وجود ذخیره سازی امن، در مخزن اعتبار سیستم عامل ذخیره می کند. اگر ترجیح می دهید از ورود محلی CLI استفاده کنید، این فیلد را خالی بگذارید و با kimi login یا /login وارد شوید." + : "اگر می خواهید Rowl نشست های Kimi را مستقیم شروع کند، این کلید را از Kimi Code Console بسازید. Rowl آن را فقط در حافظه نشست فعلی مرورگر نگه می دارد. اگر ترجیح می دهید از ورود محلی CLI استفاده کنید، این فیلد را خالی بگذارید و با kimi login یا /login وارد شوید.", resetKimiOverrides: "بازنشانی بازنویسی های Kimi", piTitle: "Pi agent harness", piDescription: - "CUT3، Pi را از طریق SDK نود خودش داخل سرور تعبیه می کند. اعتبارنامه ها و کاتالوگ مدل Pi از ~/.pi/agent خوانده می شوند؛ برای ورود از خود Pi با pi یا bunx pi استفاده کنید و /login را اجرا کنید، یا auth.json / متغیرهای محیطی Pi را پر کنید. CUT3 عمداً پکیج ها، AGENTS، system promptها، افزونه ها، مهارت ها، پرامپت ها و تم های Pi را در این مسیر غیرفعال می کند تا دستورهای فضای کاری فقط از CUT3 بیایند.", + "Rowl، Pi را از طریق SDK نود خودش داخل سرور تعبیه می کند. اعتبارنامه ها و کاتالوگ مدل Pi از ~/.pi/agent خوانده می شوند؛ برای ورود از خود Pi با pi یا bunx pi استفاده کنید و /login را اجرا کنید، یا auth.json / متغیرهای محیطی Pi را پر کنید. Rowl عمداً پکیج ها، AGENTS، system promptها، افزونه ها، مهارت ها، پرامپت ها و تم های Pi را در این مسیر غیرفعال می کند تا دستورهای فضای کاری فقط از Rowl بیایند.", modelsTitle: "مدل ها", modelsDescription: "اسلاگ های مدل اضافی را ذخیره کنید تا در انتخابگر مدل گفتگو و پیشنهادهای دستور /model دیده شوند. مدل های رایگان OpenRouter اکنون بخش مستقل خودشان را دارند.", @@ -185,16 +185,16 @@ function getSettingsCopy(language: AppLanguage) { }, openRouterFreeModelsTitle: "مدل های رایگان OpenRouter", openRouterFreeModelsDescription: (routerSlug: string) => - `CUT3 کاتالوگ زنده OpenRouter را بررسی می کند و مدل هایی را نشان می دهد که همین حالا رایگان هستند. روتر داخلی ${routerSlug} همیشه در دسترس است و می توانید هر مدل رایگان زنده را ذخیره کنید تا در انتخابگر و پیشنهادهای /model ظاهر شود.`, + `Rowl کاتالوگ زنده OpenRouter را بررسی می کند و مدل هایی را نشان می دهد که همین حالا رایگان هستند. روتر داخلی ${routerSlug} همیشه در دسترس است و می توانید هر مدل رایگان زنده را ذخیره کنید تا در انتخابگر و پیشنهادهای /model ظاهر شود.`, refreshList: "نوسازی فهرست", openRouterChecking: "در حال بررسی OpenRouter برای فهرست فعلی مدل های رایگان...", openRouterAvailable: (count: number) => - `${count} مدل رایگان زنده OpenRouter در حال حاضر با مسیر بومی ابزار CUT3 سازگار ${count === 1 ? "است" : "هستند"}، به علاوه روتر داخلی.`, + `${count} مدل رایگان زنده OpenRouter در حال حاضر با مسیر بومی ابزار Rowl سازگار ${count === 1 ? "است" : "هستند"}، به علاوه روتر داخلی.`, openRouterCached: (count: number) => - `CUT3 آخرین کاتالوگ سالم OpenRouter را نشان می دهد (${count} مدل رایگان سازگار به علاوه روتر داخلی) چون واکشی زنده فعلاً در دسترس نیست.`, + `Rowl آخرین کاتالوگ سالم OpenRouter را نشان می دهد (${count} مدل رایگان سازگار به علاوه روتر داخلی) چون واکشی زنده فعلاً در دسترس نیست.`, openRouterUnavailable: "کشف زنده مدل های رایگان OpenRouter در حال حاضر در دسترس نیست.", openRouterFilteringNote: (routerSlug: string) => - `CUT3 فقط انتخاب هایی را نشان می دهد که روی :free یا ${routerSlug} قفل شده باشند و از ابزارها پشتیبانی کنند.`, + `Rowl فقط انتخاب هایی را نشان می دهد که روی :free یا ${routerSlug} قفل شده باشند و از ابزارها پشتیبانی کنند.`, lastCheckedAt: (label: string) => `آخرین بررسی در ${label}.`, builtIn: "داخلی", saved: "ذخیره شده", @@ -257,7 +257,7 @@ function getSettingsCopy(language: AppLanguage) { desktopNotifications: "اعلان‌های دسکتاپ", desktopNotificationsDescription: "وقتی agent کارش تمام شد و پنجره فعال نیست، یک اعلان دسکتاپ نشان بده.", - desktopNotificationsGranted: "اعلان‌های مرورگر برای CUT3 مجاز هستند.", + desktopNotificationsGranted: "اعلان‌های مرورگر برای Rowl مجاز هستند.", desktopNotificationsBlocked: "اعلان‌ها در تنظیمات مرورگر یا سایت مسدود شده‌اند. برای استفاده، دوباره آن‌ها را مجاز کنید.", desktopNotificationsUnsupported: "این محیط از اعلان‌های دسکتاپ پشتیبانی نمی‌کند.", @@ -282,11 +282,11 @@ function getSettingsCopy(language: AppLanguage) { modelTooLong: (maxLength: number) => `اسلاگ مدل باید حداکثر ${maxLength} کاراکتر باشد.`, customModelAlreadySaved: "این مدل سفارشی قبلا ذخیره شده است.", openRouterMustBeFree: - "شناسه های مدل OpenRouter باید از openrouter/free یا یک اسلاگ صریح :free استفاده کنند تا CUT3 ناخواسته به مدل پولی منتقل نشود.", + "شناسه های مدل OpenRouter باید از openrouter/free یا یک اسلاگ صریح :free استفاده کنند تا Rowl ناخواسته به مدل پولی منتقل نشود.", openRouterNotInCatalog: "این مدل OpenRouter در کاتالوگ زنده فعلی رایگان وجود ندارد. فهرست را نوسازی کنید و یک مدل :free فعلی انتخاب کنید.", openRouterNeedsTools: - "CUT3 به مدل های OpenRouter نیاز دارد که هم tools و هم tool_choice را اعلام کنند. یک مدل رایگان دیگر انتخاب کنید یا از openrouter/free استفاده کنید.", + "Rowl به مدل های OpenRouter نیاز دارد که هم tools و هم tool_choice را اعلام کنند. یک مدل رایگان دیگر انتخاب کنید یا از openrouter/free استفاده کنید.", noEditorsFound: "هیچ ویرایشگری در دسترس نیست.", openKeybindingsFailed: "باز کردن فایل کلیدهای میانبر ممکن نشد.", openRouterWarningMissingCatalog: "دیگر در کاتالوگ زنده رایگان فعلی OpenRouter دیده نمی شود.", @@ -315,7 +315,7 @@ function getSettingsCopy(language: AppLanguage) { blurDescription: "Increase blur to soften detailed wallpapers behind message content.", resetImageEffects: "Reset image effects", imageStorageNote: (sizeLabel: string) => - `CUT3 stores this image in local app settings on this device. Keep it at or under ${sizeLabel}.`, + `Rowl stores this image in local app settings on this device. Keep it at or under ${sizeLabel}.`, chooseImageFile: "Choose an image file.", imageTooLarge: (sizeLabel: string) => `Choose an image up to ${sizeLabel} so it can be saved locally.`, @@ -333,12 +333,12 @@ function getSettingsCopy(language: AppLanguage) { resetCodexOverrides: "Reset codex overrides", openRouterTitle: "OpenRouter", openRouterDescription: - "CUT3 exposes OpenRouter as its own top-level UI section and routes those sessions through Codex under the hood, so you can use the built-in openrouter/free router or saved OpenRouter :free model ids without editing your normal Codex config.", + "Rowl exposes OpenRouter as its own top-level UI section and routes those sessions through Codex under the hood, so you can use the built-in openrouter/free router or saved OpenRouter :free model ids without editing your normal Codex config.", openRouterApiKey: "OpenRouter API key", openRouterKeyDescription: (electron: boolean) => electron - ? "Needed only for Codex models routed through OpenRouter. CUT3 keeps it in the desktop session and persists it in your OS credential store when secure storage is available. Use openrouter/free for the current free-model pool, or add specific :free slugs below." - : "Needed only for Codex models routed through OpenRouter. CUT3 keeps it only in memory for the current browser session. Use openrouter/free for the current free-model pool, or add specific :free slugs below.", + ? "Needed only for Codex models routed through OpenRouter. Rowl keeps it in the desktop session and persists it in your OS credential store when secure storage is available. Use openrouter/free for the current free-model pool, or add specific :free slugs below." + : "Needed only for Codex models routed through OpenRouter. Rowl keeps it only in memory for the current browser session. Use openrouter/free for the current free-model pool, or add specific :free slugs below.", openRouterConfigured: "OpenRouter key is configured for new Codex sessions.", openRouterMissing: "Add a key to use OpenRouter-routed Codex models.", resetOpenRouterKey: "Reset OpenRouter key", @@ -350,24 +350,24 @@ function getSettingsCopy(language: AppLanguage) { resetCopilotOverrides: "Reset copilot overrides", opencodeTitle: "OpenCode", opencodeDescription: - "Applies to new OpenCode sessions and lets CUT3 use a non-default `opencode` install. Manage credentials with `opencode auth login` and `opencode auth logout`, and add the top-level OpenRouter key in CUT3 if your OpenCode config expects `OPENROUTER_API_KEY`.", + "Applies to new OpenCode sessions and lets Rowl use a non-default `opencode` install. Manage credentials with `opencode auth login` and `opencode auth logout`, and add the top-level OpenRouter key in Rowl if your OpenCode config expects `OPENROUTER_API_KEY`.", opencodeBinaryPath: "Binary path", leaveBlankOpencode: "Leave blank to use opencode from your PATH.", resetOpencodeOverrides: "Reset overrides", kimiTitle: "Kimi Code CLI", kimiDescription: - "These overrides apply to new Kimi Code sessions. Install with curl -LsSf https://code.kimi.com/install.sh | bash, then authenticate with `kimi login` or the in-shell `/login` flow, or add a Kimi Code API key here to let CUT3 start sessions directly.", + "These overrides apply to new Kimi Code sessions. Install with curl -LsSf https://code.kimi.com/install.sh | bash, then authenticate with `kimi login` or the in-shell `/login` flow, or add a Kimi Code API key here to let Rowl start sessions directly.", kimiBinaryPath: "Kimi binary path", leaveBlankKimi: "Leave blank to use kimi from your PATH.", kimiApiKey: "Kimi API key", kimiApiDescription: (electron: boolean) => electron - ? "Generate this from the Kimi Code Console if you want CUT3 to start Kimi sessions directly. CUT3 keeps it in the desktop session and persists it in your OS credential store when secure storage is available. Leave this blank if you prefer to authenticate in the local CLI with `kimi login` or `/login`." - : "Generate this from the Kimi Code Console if you want CUT3 to start Kimi sessions directly. CUT3 keeps it only in memory for the current browser session. Leave this blank if you prefer to authenticate in the local CLI with `kimi login` or `/login`.", + ? "Generate this from the Kimi Code Console if you want Rowl to start Kimi sessions directly. Rowl keeps it in the desktop session and persists it in your OS credential store when secure storage is available. Leave this blank if you prefer to authenticate in the local CLI with `kimi login` or `/login`." + : "Generate this from the Kimi Code Console if you want Rowl to start Kimi sessions directly. Rowl keeps it only in memory for the current browser session. Leave this blank if you prefer to authenticate in the local CLI with `kimi login` or `/login`.", resetKimiOverrides: "Reset kimi overrides", piTitle: "Pi agent harness", piDescription: - "CUT3 embeds Pi through its Node SDK. Pi credentials and model discovery come from ~/.pi/agent; authenticate Pi through the Pi CLI (`pi` or `bunx pi`) and `/login`, or populate Pi's auth.json / provider environment variables. CUT3 intentionally disables Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes on this path so workspace instructions still come only from CUT3.", + "Rowl embeds Pi through its Node SDK. Pi credentials and model discovery come from ~/.pi/agent; authenticate Pi through the Pi CLI (`pi` or `bunx pi`) and `/login`, or populate Pi's auth.json / provider environment variables. Rowl intentionally disables Pi packages, AGENTS files, system prompts, extensions, skills, prompt templates, and themes on this path so workspace instructions still come only from Rowl.", modelsTitle: "Models", modelsDescription: "Save additional provider model slugs so they appear in the chat model picker and /model command suggestions. OpenRouter free models now have their own section, while the cards below handle additional provider-specific custom models.", @@ -388,16 +388,16 @@ function getSettingsCopy(language: AppLanguage) { }, openRouterFreeModelsTitle: "OpenRouter Free Models", openRouterFreeModelsDescription: (routerSlug: string) => - `CUT3 checks OpenRouter's live catalog and lists the models that are free right now. The built-in ${routerSlug} router is always available, and you can save any live free model below so it shows up in the picker and /model suggestions.`, + `Rowl checks OpenRouter's live catalog and lists the models that are free right now. The built-in ${routerSlug} router is always available, and you can save any live free model below so it shows up in the picker and /model suggestions.`, refreshList: "Refresh list", openRouterChecking: "Checking OpenRouter for the current free-model list...", openRouterAvailable: (count: number) => - `${count} live OpenRouter free model${count === 1 ? " is" : "s are"} currently compatible with CUT3's native tool-calling path, plus the built-in router.`, + `${count} live OpenRouter free model${count === 1 ? " is" : "s are"} currently compatible with Rowl's native tool-calling path, plus the built-in router.`, openRouterCached: (count: number) => - `CUT3 is showing the last known-good OpenRouter catalog (${count} compatible free model${count === 1 ? "" : "s"} plus the built-in router) because the live fetch is currently unavailable.`, + `Rowl is showing the last known-good OpenRouter catalog (${count} compatible free model${count === 1 ? "" : "s"} plus the built-in router) because the live fetch is currently unavailable.`, openRouterUnavailable: "Live OpenRouter free-model discovery is currently unavailable.", openRouterFilteringNote: (routerSlug: string) => - `CUT3 only lists OpenRouter picks that are locked to :free or ${routerSlug} and advertise tool use.`, + `Rowl only lists OpenRouter picks that are locked to :free or ${routerSlug} and advertise tool use.`, lastCheckedAt: (label: string) => `Last checked at ${label}.`, builtIn: "Built in", saved: "Saved", @@ -457,7 +457,7 @@ function getSettingsCopy(language: AppLanguage) { desktopNotifications: "Desktop notifications", desktopNotificationsDescription: "Show a desktop notification when the agent finishes a task and the window is not focused.", - desktopNotificationsGranted: "Browser notifications are allowed for CUT3.", + desktopNotificationsGranted: "Browser notifications are allowed for Rowl.", desktopNotificationsBlocked: "Notifications are blocked in your browser or site settings. Re-enable them there to use this feature.", desktopNotificationsUnsupported: "Desktop notifications are not supported in this environment.", @@ -483,11 +483,11 @@ function getSettingsCopy(language: AppLanguage) { modelTooLong: (maxLength: number) => `Model slugs must be ${maxLength} characters or less.`, customModelAlreadySaved: "That custom model is already saved.", openRouterMustBeFree: - "OpenRouter model ids must use openrouter/free or an explicit :free slug so CUT3 cannot drift onto a billed model.", + "OpenRouter model ids must use openrouter/free or an explicit :free slug so Rowl cannot drift onto a billed model.", openRouterNotInCatalog: "That OpenRouter model is not in the current live free catalog. Refresh the list and pick a currently free :free model.", openRouterNeedsTools: - "CUT3 requires OpenRouter models that advertise both tools and tool_choice. Pick another listed free model or use openrouter/free.", + "Rowl requires OpenRouter models that advertise both tools and tool_choice. Pick another listed free model or use openrouter/free.", noEditorsFound: "No available editors found.", openKeybindingsFailed: "Unable to open keybindings file.", openRouterWarningMissingCatalog: "No longer appears in OpenRouter's current live free catalog.", diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index b7092423f39..9dc12e5cca0 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; import ThreadSidebar from "../components/Sidebar"; +import RightSidebar, { RightSidebarProvider } from "../components/right-sidebar/RightSidebar"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { isTerminalFocused } from "../lib/terminalFocus"; @@ -15,6 +16,7 @@ import { useThreadSelectionStore } from "../threadSelectionStore"; import { Sidebar, SidebarProvider, useSidebar } from "~/components/ui/sidebar"; import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic"; import { useAppSettings } from "~/appSettings"; +import { readNativeApi } from "~/nativeApi"; import { CommandPalette, PALETTE_ICONS, @@ -246,6 +248,36 @@ function ChatRouteLayoutContent() { keywords: "settings preferences configuration options", action: () => void navigate({ to: "/settings" }), }, + { + id: "compact", + label: appSettings.language === "fa" ? "کاهش رشته" : "Compact Thread", + description: + routeThreadId === null + ? threadScopedActionDescription + : appSettings.language === "fa" + ? "زمینه‌ی رشته را برای ادامه‌ی مکالمه کاهش می‌دهد" + : "Reduce thread context to continue the conversation", + icon: PALETTE_ICONS.compact, + keywords: "compact reduce context summarize", + disabled: routeThreadId === null, + action: () => { + if (routeThreadId === null) return; + const api = readNativeApi(); + if (!api) return; + void api.threads.compact({ threadId: routeThreadId }); + }, + }, + { + id: "search", + label: appSettings.language === "fa" ? "جستجو" : "Search", + description: + appSettings.language === "fa" ? "جستجوی پروژه‌ها و رشته‌ها" : "Search projects and threads", + icon: PALETTE_ICONS.search, + keywords: "search find filter", + action: () => { + toggleSidebar(); + }, + }, ); return actions; @@ -316,7 +348,7 @@ function ChatRouteLayoutContent() { }, [keybindings, toggleSidebar]); return ( - <> + - +
+ + +
- +
); } diff --git a/apps/web/src/serverConnectionBannerCopy.test.ts b/apps/web/src/serverConnectionBannerCopy.test.ts index 8b64900a5f2..748209c6388 100644 --- a/apps/web/src/serverConnectionBannerCopy.test.ts +++ b/apps/web/src/serverConnectionBannerCopy.test.ts @@ -9,7 +9,7 @@ describe("getServerConnectionBannerTitle", () => { it("uses a desktop-specific connecting title in Electron", () => { expect( getServerConnectionBannerTitle({ retrying: false, isElectron: true, language: "en" }), - ).toContain("Connecting to CUT3"); + ).toContain("Connecting to Rowl"); }); it("keeps the local server title in the browser", () => { @@ -38,7 +38,7 @@ describe("getServerConnectionBannerDescription", () => { it("uses desktop-specific retry guidance in Electron", () => { expect( getServerConnectionBannerDescription({ retrying: true, isElectron: true, language: "en" }), - ).toContain("restart CUT3"); + ).toContain("restart Rowl"); }); it("keeps dev-server retry guidance in the browser", () => { diff --git a/apps/web/src/serverConnectionBannerCopy.ts b/apps/web/src/serverConnectionBannerCopy.ts index adddc4d292c..7299665098c 100644 --- a/apps/web/src/serverConnectionBannerCopy.ts +++ b/apps/web/src/serverConnectionBannerCopy.ts @@ -29,7 +29,7 @@ export function getServerConnectionBannerDescription(args: { if (args.language === "fa") { if (args.retrying) { return args.isElectron - ? "برنامه به صورت خودکار دوباره اتصال وب سوکت را امتحان می کند. اگر این وضعیت ادامه داشت، CUT3 را دوباره راه اندازی کنید." + ? "برنامه به صورت خودکار دوباره اتصال وب سوکت را امتحان می کند. اگر این وضعیت ادامه داشت، Rowl را دوباره راه اندازی کنید." : "برنامه به صورت خودکار دوباره اتصال وب سوکت را امتحان می کند. اگر این وضعیت ادامه داشت، سرور توسعه محلی را دوباره راه اندازی کنید."; } @@ -40,7 +40,7 @@ export function getServerConnectionBannerDescription(args: { if (args.retrying) { return args.isElectron - ? "The app is retrying the websocket connection automatically. If this keeps happening, restart CUT3." + ? "The app is retrying the websocket connection automatically. If this keeps happening, restart Rowl." : "The app is retrying the websocket connection automatically. If this keeps happening, restart the local dev server."; } diff --git a/apps/web/src/sidebarPreferencesStore.ts b/apps/web/src/sidebarPreferencesStore.ts index bb10af20f62..44a85c56cc5 100644 --- a/apps/web/src/sidebarPreferencesStore.ts +++ b/apps/web/src/sidebarPreferencesStore.ts @@ -3,7 +3,7 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { filterArchivedIds, type SidebarProjectSortMode } from "./lib/threadOrdering"; -const SIDEBAR_PREFERENCES_STORAGE_KEY = "cut3:sidebar-preferences:v1"; +const SIDEBAR_PREFERENCES_STORAGE_KEY = "rowl:sidebar-preferences:v1"; interface PersistedSidebarPreferencesState { pinnedProjectIds: ProjectId[]; diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index eb4f95becf2..811f13c0a4d 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -18,6 +18,7 @@ function makeThread(overrides: Partial = {}): Thread { codexThreadId: null, projectId: ProjectId.makeUnsafe("project-1"), title: "Thread", + goal: null, provider: "codex", model: "gpt-5-codex", runtimeMode: DEFAULT_RUNTIME_MODE, @@ -58,6 +59,7 @@ function makeReadModelThread(overrides: Partial id.trim()).filter((id) => id.length > 0))]; diff --git a/apps/web/src/threadExport.test.ts b/apps/web/src/threadExport.test.ts index 4b3441bb38c..6c40493bd97 100644 --- a/apps/web/src/threadExport.test.ts +++ b/apps/web/src/threadExport.test.ts @@ -19,6 +19,7 @@ const thread: Thread = { codexThreadId: null, projectId: project.id, title: "Export Thread!", + goal: null, provider: "codex", model: "gpt-5-codex", runtimeMode: "approval-required", diff --git a/apps/web/src/threadForking.test.ts b/apps/web/src/threadForking.test.ts index e7e8fda1870..c1ed3674251 100644 --- a/apps/web/src/threadForking.test.ts +++ b/apps/web/src/threadForking.test.ts @@ -9,6 +9,7 @@ function makeThread(overrides?: Partial): Thread { codexThreadId: null, projectId: "project-1" as never, title: "Thread", + goal: null, provider: "opencode", model: "anthropic/claude-sonnet-4", runtimeMode: "approval-required", diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index 4912a055ac2..2848d4700e2 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -89,6 +89,7 @@ export interface Thread { codexThreadId: string | null; projectId: ProjectId; title: string; + goal: string | null; provider?: ProviderKind; model: string; runtimeMode: RuntimeMode; diff --git a/apps/web/src/worktreeCleanup.test.ts b/apps/web/src/worktreeCleanup.test.ts index 7ce23bb6e9a..ada97ad7074 100644 --- a/apps/web/src/worktreeCleanup.test.ts +++ b/apps/web/src/worktreeCleanup.test.ts @@ -10,6 +10,7 @@ function makeThread(overrides: Partial = {}): Thread { codexThreadId: null, projectId: ProjectId.makeUnsafe("project-1"), title: "Thread", + goal: null, model: "gpt-5.3-codex", runtimeMode: DEFAULT_RUNTIME_MODE, interactionMode: DEFAULT_INTERACTION_MODE, @@ -79,16 +80,16 @@ describe("getOrphanedWorktreePathForThread", () => { describe("formatWorktreePathForDisplay", () => { it("shows only the last path segment for unix-like paths", () => { const result = formatWorktreePathForDisplay( - "/Users/julius/.t3/worktrees/cut3-mvp/cut3-4e609bb8", + "/Users/julius/.t3/worktrees/rowl-mvp/rowl-4e609bb8", ); - expect(result).toBe("cut3-4e609bb8"); + expect(result).toBe("rowl-4e609bb8"); }); it("normalizes windows separators before selecting the final segment", () => { const result = formatWorktreePathForDisplay( - "C:\\Users\\julius\\.t3\\worktrees\\cut3-mvp\\cut3-4e609bb8", + "C:\\Users\\julius\\.t3\\worktrees\\rowl-mvp\\rowl-4e609bb8", ); - expect(result).toBe("cut3-4e609bb8"); + expect(result).toBe("rowl-4e609bb8"); }); it("uses the final segment even when outside ~/.t3/worktrees", () => { diff --git a/apps/web/src/wsNativeApi.ts b/apps/web/src/wsNativeApi.ts index b144e6940d9..5e045019cf2 100644 --- a/apps/web/src/wsNativeApi.ts +++ b/apps/web/src/wsNativeApi.ts @@ -21,6 +21,7 @@ import { ProjectListSkillsResult, ProjectSearchEntriesResult, ProjectWriteFileResult, + ProjectDeleteFileResult, ServerConfigUpdatedPayload, ServerConfig, ServerCopilotReasoningProbe, @@ -197,6 +198,8 @@ export function createWsNativeApi(): NativeApi { requestWithSchema(WS_METHODS.projectsListSkills, ProjectListSkillsResult, input), writeFile: (input) => requestWithSchema(WS_METHODS.projectsWriteFile, ProjectWriteFileResult, input), + deleteFile: (input) => + requestWithSchema(WS_METHODS.projectsDeleteFile, ProjectDeleteFileResult, input), }, threads: { getShareStatus: (input) => diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index af635bf145b..d64bc1e7ed8 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -9,7 +9,7 @@ import pkg from "./package.json" with { type: "json" }; const webRoot = fileURLToPath(new URL(".", import.meta.url)); const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); const port = Number(process.env.PORT ?? 5733); -const sourcemapEnv = process.env.CUT3_WEB_SOURCEMAP?.trim().toLowerCase(); +const sourcemapEnv = process.env.ROWL_WEB_SOURCEMAP?.trim().toLowerCase(); const devUrlEnv = process.env.VITE_DEV_SERVER_URL?.trim(); const devServerHost = (() => { if (!devUrlEnv) { diff --git a/bun.lock b/bun.lock index c9e20aedf8f..28c3ac26a3d 100644 --- a/bun.lock +++ b/bun.lock @@ -42,10 +42,10 @@ }, }, "apps/server": { - "name": "cut3", + "name": "rowl", "version": "1.0.1", "bin": { - "cut3": "./dist/index.mjs", + "rowl": "./dist/index.mjs", }, "dependencies": { "@agentclientprotocol/sdk": "^0.15.0", @@ -1260,8 +1260,6 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "cut3": ["cut3@workspace:apps/server"], - "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -2022,6 +2020,8 @@ "rolldown-plugin-dts": ["rolldown-plugin-dts@0.22.5", "", { "dependencies": { "@babel/generator": "8.0.0-rc.2", "@babel/helper-validator-identifier": "8.0.0-rc.2", "@babel/parser": "8.0.0-rc.2", "@babel/types": "8.0.0-rc.2", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.6", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-rc.3", "typescript": "^5.0.0 || ^6.0.0-beta", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-M/HXfM4cboo+jONx9Z0X+CUf3B5tCi7ni+kR5fUW50Fp9AlZk0oVLesibGWgCXDKFp5lpgQ9yhKoImUFjl3VZw=="], + "rowl": ["rowl@workspace:apps/server"], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], diff --git a/docs/FEATURE_SPECIFICATIONS.md b/docs/FEATURE_SPECIFICATIONS.md new file mode 100644 index 00000000000..563fa8c8b1c --- /dev/null +++ b/docs/FEATURE_SPECIFICATIONS.md @@ -0,0 +1,443 @@ +# Rowl Feature Specifications + +This document contains detailed specifications for all Rowl features. When a user proposes a simple idea, it should be expanded here into a detailed spec that the AI can reference and never forget. + +--- + +## Feature Status Dashboard + +**Rule: A feature is NOT done until it's fully wired to real data/APIs. Mock UI = 0%.** + +| Feature | Backend | Frontend | Overall | Usable? | +| ----------------------- | ------- | -------- | ------- | ------- | +| Right Sidebar Shell | N/A | 15% | 15% | ❌ | +| PM Chat | 0% | 0% | 0% | ❌ | +| Threads Tab | 0% | 0% | 0% | ❌ | +| Features Board | 0% | 0% | 0% | ❌ | +| Goals Tab | 0% | 0% | 0% | ❌ | +| Context System | 0% | 0% | 0% | ❌ | +| Thread Goal Statement | 100% | 100% | 100% | ✅ | +| Project Brief | 0% | 0% | 0% | ❌ | +| Settings Reorganization | 0% | 0% | 0% | ❌ | +| Skills AI Creation | 0% | 0% | 0% | ❌ | +| Overseer | 0% | 0% | 0% | ❌ | + +**Only 1 feature fully done.** Everything else is 0% or mock-only. + +--- + +## Completion Percentage Guide + +| % | Meaning | +| ------ | --------------------------------------------------- | +| 0% | Not started, nothing exists | +| 1-20% | Contracts/schemas exist, nothing works | +| 21-40% | Backend service interfaces exist, no implementation | +| 41-60% | Backend implementation done, frontend needs wiring | +| 61-80% | Frontend wired, needs testing/integration | +| 81-99% | Testing/fixing, almost there | +| 100% | Fully working, tested, merged to main | + +--- + +## Feature: Right Sidebar Shell + +**Overall: 15%** - Shell integrated into layout, empty content area + +### Backend: N/A + +Just a UI container, no backend needed. + +### Frontend + +- [x] RightSidebar.tsx shell with tab bar (136 lines) + - Collapsible (~320px expanded, ~40px collapsed) + - 5 tab slots defined (pm-chat, threads, features, goals, context) + - Tab icons and labels working + - Empty content area (waiting for tab implementations) +- [x] Integrated into `_chat.tsx` layout + - Renders after Outlet (main chat content) + - Border styling applied + +- [ ] Tab content slots (0%) - Nothing rendering in main area + +--- + +## Feature: PM Chat (in Right Sidebar) + +**Overall: 0%** - Never started + +### Summary + +AI Product Manager chat integrated into right sidebar. Has full context of project (threads, features, goals, context) and coordinates all work. + +### What Needs Building + +#### Backend (0%) + +- [ ] PMChatContextService implementation + - Aggregates all project data for PM + - Fetches threads, features, goals, context nodes for a project + - No persistence needed - reads from existing projections + +#### Frontend (0%) + +- [ ] PMChat component (0%) + - Chat interface for AI PM + - WebSocket connection to backend + - Context display (threads, features, goals visible to PM) + - Actions: create thread, update feature, assign work + +### Implementation Order + +1. Backend: PMChatContextService (reads existing data) +2. Frontend: PMChat component wired to PMChatContextService + +--- + +## Feature: Threads Tab (in Right Sidebar) + +**Overall: 0%** - Never started + +### Summary + +List of all threads in the current project with their goal statements and status. + +### What Needs Building + +#### Backend (0%) + +- [ ] Use existing thread projection data +- [ ] Filter threads by current project +- [ ] Fetch thread goals (already exists in schema) + +#### Frontend (0%) + +- [ ] ThreadsTab component (0%) + - List threads from current project + - Show thread goal statement + - Show status indicator (working, connecting, etc.) + - Click to switch threads + - Uses existing thread data from orchestrator + +### Implementation Order + +1. Backend: Query existing thread projection by projectId +2. Frontend: ThreadsTab wired to thread data + +--- + +## Feature: Features Board (in Right Sidebar) + +**Overall: 0%** - Never started (mock UI was deleted) + +### Summary + +Kanban-style board with columns: Backlog, In Progress, Done, Wishlist. Each feature has detailed spec. + +### What Needs Building + +#### Backend (0%) + +- [ ] FeatureService implementation + - CRUD for features + - Stages: "backlog" | "in_progress" | "done" | "wishlist" + - Fields: id, projectId, name, description, stage, threadId, createdAt, updatedAt, createdBy + - Needs new projection: ProjectionFeatures + +#### Frontend (0%) + +- [ ] FeaturesBoard component (0%) + - Kanban columns with drag-drop + - Feature cards (name, description, thread) + - Create/edit feature UI + - Wire to FeatureService API + +### Contracts (Exist, 100%) + +- `packages/contracts/src/features.ts` - Schemas done +- `packages/contracts/src/ws.ts` - WebSocket methods defined + +### Implementation Order + +1. Backend: ProjectionFeatures + FeatureService +2. Frontend: FeaturesBoard component with drag-drop + +--- + +## Feature: Goals Tab (in Right Sidebar) + +**Overall: 0%** - Never started + +### Summary + +Project-level goals display with main goal prominent and sub-goals linked to threads. + +### What Needs Building + +#### Backend (0%) + +- [ ] GoalsService implementation + - CRUD for goals + - Fields: id, projectId, text, isMain, linkedThreadIds, createdAt + - Set/unset main goal + - Link/unlink threads to goals + - Needs new projection: ProjectionGoals + +#### Frontend (0%) + +- [ ] GoalsTab component (0%) + - Main goal prominently displayed + - Sub-goals list + - Visual progress indicators + - Wire to GoalsService API + +### Contracts (Exist, 100%) + +- `packages/contracts/src/goals.ts` - Schemas done +- `packages/contracts/src/ws.ts` - WebSocket methods defined + +### Implementation Order + +1. Backend: ProjectionGoals + GoalsService +2. Frontend: GoalsTab component + +--- + +## Feature: Context System (in Right Sidebar) + +**Overall: 0%** - Never started + +### Summary + +Visual representation of context chunks as nodes that can be managed to achieve context reduction. Called "Context" not "Tombstone". + +### What Needs Building + +#### Backend (0%) + +- [ ] ContextService implementation + - CRUD for context nodes + - Fields: id, projectId, threadId, type, summary, size, compressed, createdAt + - Types: "messages" | "file" | "artifact" | "memory" + - Compress/restore context nodes + - Calculate context budget + - Needs new projection: ProjectionContextNodes + +#### Frontend (0%) + +- [ ] ContextTab component (0%) + - Node-based visualizer + - Show context chunks as nodes + - Compression status (active vs compressed) + - Compress/restore buttons + - Context budget display + - Wire to ContextService API + +### Contracts (Exist, 100%) + +- `packages/contracts/src/context.ts` - Schemas done +- `packages/contracts/src/ws.ts` - WebSocket methods defined + +### Implementation Order + +1. Backend: ProjectionContextNodes + ContextService +2. Frontend: ContextTab component with visualizer + +--- + +## Feature: Thread Goal Statement + +**Overall: 100%** ✅ DONE + +### Backend: 100% + +- [x] `goal` field added to `OrchestrationThread` schema +- [x] `goal` field added to `ThreadMetaUpdateCommand` +- [x] Uses existing `thread.meta.update` command + +### Frontend: 100% + +- [x] `ThreadGoalStatement` component (`apps/web/src/components/chat/ThreadGoalStatement.tsx`) +- [x] Displayed above MessagesTimeline in ChatView +- [x] Click to edit, auto-saves on blur/Enter +- [x] "Saved" indicator after save +- [x] All tests updated with `goal: null` + +### Implementation Location + +- `packages/contracts/src/orchestration.ts` (lines ~275, ~387) +- `apps/web/src/components/chat/ThreadGoalStatement.tsx` (new file) +- `apps/web/src/types.ts` (Thread interface) +- `apps/web/src/store.ts` (syncServerReadModel) +- `apps/web/src/components/ChatView.tsx` + +--- + +## Feature: Project Brief + +**Overall: 0%** - Never started + +### Summary + +A detailed description of the project's purpose, goals, and context. Stored in `.rowl/project-brief.md`. + +### What Needs Building + +#### Backend (0%) + +- [ ] ProjectBriefService implementation + - Read/write `.rowl/project-brief.md` + - Fields: projectId, brief (markdown), filePath, lastEditedAt, lastEditedByThreadId + - File system operations + +#### Frontend (0%) + +- [ ] ProjectBrief component (0%) + - Markdown editor with preview + - Accessible from project settings or PM chat + - Auto-save to file + +### Implementation Order + +1. Backend: ProjectBriefService (file read/write) +2. Frontend: ProjectBrief editor component + +--- + +## Feature: Settings Reorganization + +**Overall: 0%** - Never started + +### Summary + +Replace long-scrolling settings page with tabbed interface. Unified model management. + +### What Needs Building + +#### Backend (0%) + +- [ ] Lazy provider health checks + - Don't run on startup + - Run when user opens Models tab + - `serverRefreshProviderHealth` method (already defined in ws.ts) + +#### Frontend (0%) + +- [ ] Tabbed settings interface + - General, Models, Providers, Keybindings, Safety tabs + - Unified Models tab (replaces ManageModelsDialog) + - Provider errors shown lazily (not on startup) + +### Implementation Order + +1. Backend: Ensure lazy health checks work +2. Frontend: Tabbed settings UI + +--- + +## Feature: Skills AI Creation + +**Overall: 0%** - Never started + +### Summary + +AI-assisted creation of SKILL.md files for project-specific instructions. + +### What Needs Building + +#### Backend (0%) + +- [ ] SkillService implementation + - Analyze project structure + - Generate skill suggestions + - Read/write `.rowl/skills/SKILL-name.md` + +#### Frontend (0%) + +- [ ] Skill creation UI (0%) + - Accessible from PM chat or project settings + - AI analyzes project, suggests skills + - User edits in preview, saves + +### Implementation Order + +1. Backend: SkillService +2. Frontend: Skill creation UI + +--- + +## Feature: Overseer (Guardian System) + +**Overall: 0%** - Never started + +### Summary + +Background AI monitoring system that watches provider output for capability forgetfulness patterns. + +### What Needs Building + +#### Backend (0%) + +- [ ] GuardianService implementation + - Monitor AI outputs for patterns: + - "I can't do that" when AI actually can + - "You need to run this yourself" + - Loop detection + - Stuck task detection + - GuardianSuggestion schema exists in specs + +#### Frontend (0%) + +- [ ] Guardian panel (0%) + - Collapsible panel in ChatView + - Show suggestions when capability forgetfulness detected + - User can acknowledge/dismiss + +### Implementation Order + +1. Backend: GuardianService with pattern matching +2. Frontend: Guardian panel UI + +--- + +## Process Rules + +1. **Feature is NOT done until:** + - Backend implementation complete and tested + - Frontend wired to real APIs (not mock data) + - Merged to main branch + - Typecheck and lint pass + +2. **Before starting a feature:** + - Read this document + - Understand current % completion + - Start with backend, then frontend + +3. **When feature reaches 100%:** + - Update status table at top + - Update implementation location notes + - Commit with message: "feat: complete [feature name]" + +--- + +## Development Order (Recommended) + +Based on dependencies: + +1. **Thread Goal Statement** ✅ Already done +2. **PM Chat** - Depends on: threads data, features, goals, context (needs all of below) +3. **Threads Tab** - Uses existing thread data (easy) +4. **Goals Tab** - Needs GoalsService (medium) +5. **Features Board** - Needs FeatureService (medium) +6. **Context System** - Needs ContextService (complex) +7. **Project Brief** - Independent +8. **Settings Reorganization** - Independent +9. **Skills AI Creation** - Independent +10. **Overseer** - Independent + +**Recommended next:** Threads Tab (easiest, uses existing data) or Goals Tab (medium, needs new service) + +--- + +_Last updated: 2026-04-08_ diff --git a/docs/ROWL_ROADMAP.md b/docs/ROWL_ROADMAP.md new file mode 100644 index 00000000000..5eab4ba23fe --- /dev/null +++ b/docs/ROWL_ROADMAP.md @@ -0,0 +1,580 @@ +# ROWL Roadmap + +## Implementation Roadmap for the Agentic Engineering Control Room + +This document outlines the phased implementation plan for Rowl, moving from the current CUT3 foundation to the full vision of a multi-provider, multi-project, simultaneous agent workspace. + +--- + +## Phase 1: Context Management Foundation (Priority: CRITICAL) + +### Why First? + +Context is the foundation of everything. Without proper context management, all other features suffer. The AI cannot work effectively with poor context, and users cannot trust outputs they don't understand. + +### Features + +#### 1.1 Tombstone Context Trimming + +**Problem:** Current `/compact` command uses basic summarization which loses nuance and doesn't show users what was removed. + +**Valo Solution:** Surgical trimming with "tombstones" - markers that show exactly what was trimmed, allowing retrieval and maintaining conversation coherence. + +**Implementation:** + +``` +Location: apps/server/src/orchestration/Services/ContextManager.ts (new) + +Interface: +- trimContext(messages, budget): { trimmed: Message[], tombstones: Tombstone[] } +- restoreFromTombstone(tombstoneId): Message +- getContextBudget(threadId): ContextBudget + +Tombstone Schema: +{ + id: string + originalContent: string (full content) + trimReason: "context_limit" | "relevance" | "user_preference" + position: { before: number, after: number } + createdAt: timestamp + metadata: { tokensRemoved: number, category: "reasoning" | "tool_output" | "conversation" } +} +``` + +**Implementation Steps:** + +1. Create `packages/contracts/src/context.ts` with Tombstone schema +2. Create `apps/server/src/orchestration/Services/ContextManager.ts` with trim/restore logic +3. Modify `ProviderRuntimeIngestion.ts` to use ContextManager before sending to providers +4. Add UI for viewing/restoring tombstones in `MessagesTimeline.tsx` +5. Add `/tombstones` slash command to list and restore trimmed content + +**Complexity:** Medium +**Impact:** High (foundational for trust and context preservation) + +#### 1.2 Context Budget System + +**Problem:** No visibility into how much context is being used vs. available. + +**Implementation:** + +``` +Location: apps/server/src/orchestration/Services/ContextBudgetService.ts (new) + +Interface: +- getBudget(threadId): ContextBudget +- setBudget(threadId, limit): void +- alertThreshold(threadId, percentage): void +- getBudgetBreakdown(threadId): { system: number, history: number, attachments: number } +``` + +**UI Integration:** + +- Show context meter in composer (like a fuel gauge) +- Breakdown tooltip showing context categories +- Warning at 80%, critical at 95% + +**Complexity:** Medium +**Impact:** Medium + +#### 1.3 Selective Context Injection + +**Problem:** Sometimes you want to pull context from another thread or project. + +**Implementation:** + +- Add `/inject` slash command: `/inject thread: message:` +- Add cross-thread reference UI in message context menu +- Implement context injection as a special message type that gets expanded on send + +**Complexity:** Medium +**Impact:** Medium + +--- + +## Phase 2: Guardian System (Priority: HIGH) + +### Why Second? + +Guardian is lightweight, high-impact, and can run in the background while you work on other features. It addresses the "AI forgetting what it can do" problem that frustrates users. + +### Features + +#### 2.1 Guardian Background Service + +**Problem:** Claude and other models can "forget" their capabilities during conversation, leading to "I can't do that" when they actually can. + +**Valo Solution:** Background Haiku process watching Claude's output for patterns like "I can't do X" or "you need to run this command yourself", then reminding Claude of capabilities. + +**Implementation:** + +``` +Location: apps/server/src/guardian/Services/GuardianService.ts (new) + +Interface: +- startGuardian(sessionId): void +- stopGuardian(sessionId): void +- processOutput(sessionId, output): GuardianSuggestion[] +- acknowledgeSuggestion(suggestionId): void + +GuardianSuggestion Schema: +{ + id: string + sessionId: string + patternMatched: string + suggestion: string + capability: string + confidence: number + createdAt: timestamp + acknowledged: boolean +} +``` + +**Pattern Matching:** + +```typescript +const GUARDIAN_PATTERNS = [ + { + pattern: /I can't (?:do|run|execute|use)/i, + capability: "tool_use", + suggestion: "You have access to shell, read, write, and edit tools", + }, + { + pattern: /you(?:'ll| will) need to run this yourself/i, + capability: "automation", + suggestion: "You can execute commands directly", + }, + { + pattern: /I don't have access to.*file system/i, + capability: "file_access", + suggestion: "You have full file system access via tools", + }, + { + pattern: /can't modify.*files?/i, + capability: "file_write", + suggestion: "You can use write and edit tools to modify files", + }, +]; +``` + +**Implementation Steps:** + +1. Create `packages/contracts/src/guardian.ts` with GuardianSuggestion schema +2. Create `apps/server/src/guardian/Services/GuardianService.ts` +3. Create `apps/server/src/guardian/Layers/OutputWatcher.ts` for streaming output analysis +4. Integrate with `ProviderRuntimeIngestion.ts` to watch provider outputs +5. Add Guardian UI panel in ChatView (collapsible sidebar showing active suggestions) +6. Add `/guardian` slash command to toggle on/off + +**Complexity:** Low-Medium +**Impact:** High (directly improves AI capability utilization) + +#### 2.2 Guardian Learning + +**Problem:** Fixed patterns miss many cases. + +**Implementation:** + +- Allow users to add custom guardian patterns via settings +- Track which suggestions were accepted/rejected to learn preferences +- Store pattern preferences per-project + +**Complexity:** Medium +**Impact:** Medium + +--- + +## Phase 3: ValoVoice Pipeline (Priority: HIGH) + +### Why Third? + +Voice input is a major quality-of-life improvement for vibe coding. It's not about transcribing—it's about having a faster, more natural input that can be enhanced with codebase awareness. + +### Features + +#### 3.1 Voice Input Infrastructure + +**Problem:** Native speech-to-text is slow and not code-aware. + +**Valo Solution:** Faster Whisper + Haiku as cleanup layer. Haiku adds codebase context to transcription, translates speech patterns to proper coding terms. + +**Implementation:** + +``` +Location: apps/server/src/voice/Services/VoiceService.ts (new) + +Interface: +- startRecording(projectId): streamId +- stopRecording(streamId): AudioSegment +- transcribe(streamId, options): Transcription +- enhanceWithContext(transcription, projectContext): EnhancedTranscription + +EnhancedTranscription: +{ + original: string + enhanced: string + corrections: { original: string, corrected: string, reason: string }[] + codeReferences: { term: string, file: string, line: number }[] +} +``` + +**Implementation Steps:** + +1. Create `packages/contracts/src/voice.ts` with voice schemas +2. Create `apps/server/src/voice/Services/VoiceService.ts` with Faster Whisper integration +3. Create `apps/server/src/voice/Services/ContextEnhancer.ts` using Haiku for code-aware cleanup +4. Add WebSocket endpoint for streaming audio +5. Add voice input UI in ComposerPromptEditor (microphone button) +6. Add voice settings in Settings (model selection, language, etc.) + +**Complexity:** High (requires native module integration) +**Impact:** Very High (transforms input experience) + +#### 3.2 Voice Command Recognition + +**Problem:** Voice input should understand commands like "run this" or "explain what I just said". + +**Implementation:** + +- Add voice command prefix detection ("computer, run this") +- Map voice commands to slash commands +- Add visual feedback for recognized commands + +**Complexity:** Medium +**Impact:** Medium + +--- + +## Phase 4: Multi-Agent Simultaneous Orchestration (Priority: HIGH) + +### Why Fourth? + +This is what separates "better chat" from "control room". Running agents in parallel on complex tasks. + +### Features + +#### 4.1 Agent Session Coordinator + +**Problem:** Current architecture routes to one provider at a time per thread. + +**Implementation:** + +``` +Location: apps/server/src/multiagent/Services/AgentCoordinator.ts (new) + +Interface: +- createAgentSession(projectId, provider, config): agentSessionId +- coordinateAgents(projectId, taskGraph): CoordinationResult +- getAgentStatus(agentSessionId): AgentStatus +- terminateAgent(agentSessionId): void + +TaskGraph: +{ + nodes: { id: string, task: string, provider: ProviderType, dependsOn: string[] }[] + execution: "parallel" | "sequential" | "dependency_based" +} +``` + +**Implementation Steps:** + +1. Extend `ProviderService.ts` to support multiple concurrent sessions +2. Create `apps/server/src/multiagent/Services/AgentCoordinator.ts` +3. Create `apps/server/src/multiagent/Layers/TaskRouter.ts` for distributing tasks +4. Add multi-agent UI in project sidebar showing all active agents +5. Add agent timeline view showing task dependencies + +**Complexity:** High (requires architectural changes to provider layer) +**Impact:** Very High (enables parallel workflows) + +#### 4.2 Process Isolation + +**Problem:** Agent crashes can take down the whole system. + +**Valo Solution:** Paginated subprocess management with isolation boundaries. + +**Implementation:** + +- Create `apps/server/src/multiagent/Services/ProcessManager.ts` +- Implement subprocess pagination with event stream isolation +- Add crash recovery with state restoration +- Add resource monitoring per agent + +**Complexity:** High +**Impact:** High (reliability) + +#### 4.3 Agent Communication Protocol + +**Problem:** Agents need to coordinate without overwriting each other's context. + +**Implementation:** + +- Add inter-agent message types to `orchestration.ts` +- Create agent inbox/outbox system +- Add shared context regions for agent-to-agent communication +- Implement "agent can read X but not write" permissions + +**Complexity:** High +**Impact:** High (foundational for multi-agent) + +--- + +## Phase 5: Visual Orchestration Dashboard (Priority: MEDIUM) + +### Why Fifth? + +Visibility is a core part of the control room philosophy. Need to see everything at a glance. + +### Features + +#### 5.1 Project Dashboard + +**Problem:** Need a unified view of all projects, threads, and agents. + +**Implementation:** + +``` +Location: apps/web/src/components/Dashboard/DashboardView.tsx (new) + +Features: +- Project cards with status, last activity, active agents +- Quick actions: open project, create thread, spawn agent +- Activity feed showing recent events across all projects +- Context budget overview per project +``` + +**Implementation Steps:** + +1. Create `apps/web/src/components/Dashboard/` directory +2. Create `DashboardView.tsx` with project overview +3. Create `AgentStatusPanel.tsx` for active agent monitoring +4. Create `ActivityFeed.tsx` for real-time event stream +5. Add dashboard route in TanStack Router + +**Complexity:** Medium +**Impact:** Medium + +#### 5.2 Agent Process Monitor + +**Problem:** Can't see what agents are doing in real-time. + +**Implementation:** + +- Add streaming output panel per agent +- Show tool calls, reasoning, token usage +- Add "pause", "resume", "interrupt" controls +- Add cost tracking per agent + +**Complexity:** Medium +**Impact:** High + +#### 5.3 Timeline View + +**Problem:** Need to see the full history of a project/thread. + +**Implementation:** + +- Create horizontal timeline view of all events +- Color-coded by agent/provider +- Zoomable/pannable +- Exportable as JSON/MD + +**Complexity:** Medium +**Impact:** Medium + +--- + +## Phase 6: Apply/Review System (Priority: HIGH) + +### Why Sixth? + +Structured review is how you maintain quality with AI-generated content. + +### Features + +#### 6.1 Review Queue + +**Problem:** AI outputs go directly into the codebase without structured review. + +**Implementation:** + +``` +Location: apps/server/src/review/Services/ReviewQueue.ts (new) + +Interface: +- createReview(reviewable): reviewId +- approveReview(reviewId): void +- rejectReview(reviewId, reason): void +- requestChanges(reviewId, changes): void + +Review States: pending | approved | rejected | changes_requested +``` + +**Implementation Steps:** + +1. Create `packages/contracts/src/review.ts` with Review schemas +2. Create `apps/server/src/review/Services/ReviewQueue.ts` +3. Create review API endpoints in `wsServer.ts` +4. Add review UI panel in ChatView (shows pending reviews) +5. Add diff view for reviewing file changes + +**Complexity:** Medium +**Impact:** High (quality control) + +#### 6.2 Structured Diff Panel + +**Problem:** Current diff panel is basic. + +**Implementation:** + +- Side-by-side diff with syntax highlighting +- Inline comment threads on specific lines +- Approve/reject per-file in multi-file changes +- "Apply selected" to apply only approved files + +**Complexity:** Medium +**Impact:** Medium + +#### 6.3 Checkpoint Integration + +**Problem:** Need to rollback when reviews reject changes. + +**Implementation:** + +- Auto-create checkpoint before applying AI changes +- Add rollback UI in review panel +- Add checkpoint comparison in diff view + +**Complexity:** Low-Medium +**Impact:** High + +--- + +## Phase 7: Enhanced Testing Infrastructure (Priority: MEDIUM) + +### Why Seventh? + +Valo has 66+ tests per feature. CUT3 has minimal tests. This is a reliability gap. + +### Features + +#### 7.1 Test Coverage Goals + +**Problem:** Current test coverage is minimal. + +**Implementation:** + +- Establish minimum 66 tests per major feature +- Use multi-agent verification (researcher, reviewer, challenger agents) +- Add property-based testing for context management +- Add integration tests for WebSocket protocol + +**Test Structure:** + +``` +Feature: Context Management +├── Unit Tests +│ ├── TombstoneManager.test.ts (15 tests) +│ ├── ContextTrimmer.test.ts (20 tests) +│ └── ContextBudgetService.test.ts (10 tests) +├── Integration Tests +│ ├── ContextIntegration.test.ts (10 tests) +│ └── MultiProviderContext.test.ts (8 tests) +└── E2E Tests + └── TombstoneWorkflow.test.ts (3 tests) +``` + +**Complexity:** Medium +**Impact:** High (reliability) + +--- + +## Phase 8: Content Canvas (Future - LOW Priority) + +### Why Eighth? + +The vision extends beyond code. But code first. + +### Features + +#### 8.1 Flashboards Integration + +**Problem:** Need visual canvas for video/image workflow. + +**Implementation:** + +- Add canvas project type +- Integrate Flashboards-style drag-and-drop +- Link canvas elements to code artifacts +- Add AI generation for images/video + +**Complexity:** High +**Impact:** Low (future) + +#### 8.2 Flora Integration + +**Problem:** Need AI image generation in workflow. + +**Implementation:** + +- Add Flora as a provider +- Support image generation as a tool +- Add image review to review queue +- Link generated images to code projects + +**Complexity:** High +**Impact:** Low (future) + +--- + +## Priority Matrix + +| Phase | Feature | Complexity | Impact | Priority | +| ----- | ------------------------------- | ---------- | --------- | -------- | +| 1 | Context Management (Tombstones) | Medium | High | CRITICAL | +| 1 | Context Budget System | Medium | Medium | HIGH | +| 2 | Guardian System | Low-Medium | High | HIGH | +| 3 | ValoVoice Pipeline | High | Very High | HIGH | +| 4 | Multi-Agent Orchestration | High | Very High | HIGH | +| 5 | Visual Dashboard | Medium | Medium | MEDIUM | +| 6 | Apply/Review System | Medium | High | HIGH | +| 7 | Testing Infrastructure | Medium | High | MEDIUM | +| 8 | Content Canvas | High | Low | LOW | + +--- + +## Quick Wins (Can Start Immediately) + +1. **Guardian System** - Low complexity, high impact, can be added in 1-2 weeks +2. **Context Budget UI** - Just visualization, minimal backend changes +3. **Enhanced Diff Panel** - Mostly frontend work +4. **Slash Command Expansion** - Low complexity, high usability + +--- + +## Implementation Notes + +### Architecture Principles + +1. **Event Sourcing First** - All new features should use event sourcing like existing orchestration layer +2. **Provider Adapter Pattern** - Continue using adapters for new provider integrations +3. **Process Isolation** - Multi-agent must isolate processes to prevent cascade failures +4. **Schema Contracts** - All new features need contracts in `packages/contracts` + +### Testing Requirements + +Per AGENTS.md: + +- All new code must pass `bun run fmt`, `bun run lint`, `bun run typecheck` +- Use `bun run test` (Vitest) for all tests +- 66+ tests per major feature +- Multi-agent verification for critical paths + +### Documentation Requirements + +- Update relevant `.docs/*.md` files when adding features +- Keep `AGENTS.md` aligned with new conventions +- Document all new slash commands in README.md + +--- + +_Last updated: Based on Valo transcript analysis and Rowl codebase review_ diff --git a/docs/ROWL_VISION.md b/docs/ROWL_VISION.md new file mode 100644 index 00000000000..8c98e68e367 --- /dev/null +++ b/docs/ROWL_VISION.md @@ -0,0 +1,308 @@ +# ROWL: The Agentic Engineering Control Room + +## Vision Document + +--- + +## The Problem with Current AI Coding Tools + +Most AI coding tools treat AI as a faster autocomplete. You get a single chat, a single model, a single project context, and a single thread of conversation. When it goes off the rails, you start over. When context runs out, you're lost. When the AI "forgets" what it can do, you're stuck. + +This is not how humans work. This is not how creative work happens. This is not how engineering teams operate. + +**The problem isn't the AI. The problem is the interface between human and AI.** + +--- + +## The Vision: Rowl as an Agentic Engineering Control Room + +Rowl is not a "launcher for coding models." Rowl is a **control room for agentic engineering**. + +Just like a recording studio has: + +- Multiple tracks running simultaneously +- Visual feedback on every input/output +- The ability to record, review, and overdub +- Mixers that give you precise control over each element +- Sessions that can be saved, branched, and revisited + +Rowl gives you the same control over AI agents: + +- **Multiple providers** running simultaneously (Claude Code, Codex, Copilot, OpenCode, custom models) +- **Multiple projects** with clear boundaries and shared contexts +- **Multiple agents** working in parallel on different aspects of a problem +- **Visibility** into exactly what every agent is doing, thinking, and producing +- **Control** over context, permissions, workflows, and outputs +- **Review systems** for approving, rejecting, or modifying AI-generated work +- **Structured application** of AI outputs with full audit trails + +This is fundamentally different from "one chat with one model." + +--- + +## Core Philosophy: Vibe Coding + +The name "Rowl" captures the feeling we want: relaxed, confident, in control. This is the **vibe coding** philosophy. + +### What is Vibe Coding? + +Vibe coding is a term that captures how Valo and now Rowl approach AI-assisted engineering. It's not about letting AI do everything (that's just delegation). It's not about reviewing every line (that's just pair programming with extra steps). It's about a fundamentally different relationship between human intent and AI execution. + +**Key principles:** + +1. **Humans are Notice-ers, Not Coders** + + The most powerful skill in a vibe-coded workflow is not knowing how to code. It's knowing _what you want_. The human acts as the strategic observer—the one who notices when something feels right, when something is off, when the direction needs to shift. + + Non-coders often have an advantage here because they don't get caught up in implementation details. They see the _outcome_ rather than the _method_. + +2. **Context is Everything** + + The quality of AI output is 90% context. Give an AI 10 lines of perfect context and it will outperform an AI given 10,000 lines of poorly organized context. + + This is why Rowl's context management is surgical, not surgical-ish. We don't just compress or summarize. We **trim with precision**, leaving markers (tombstones) so you can always see what was removed and why. + +3. **Single Focused Chat > Many Scattered Chats** + + Context fragmentation is the enemy of good AI work. When you have 15 different chat windows, you have 15 different partial contexts, none of which know what the other is doing. + + Rowl's philosophy: **one focused thread per task**, with clear relationships to other threads. Not scattered conversations, but organized sessions with full history and context. + +4. **The "Why" Matters More Than the "How"** + + Traditional coding is about specifying _how_ to do something. Vibe coding is about communicating _why_ something matters. + + When you tell an AI "I want this button to feel responsive and fast," you're giving it more to work with than "add a loading state." The AI can infer the intent, the aesthetic, the user experience goal. The "how" becomes negotiable; the "why" is fixed. + +5. **AI Should Know What It Can Do** + + Claude, GPT, and other frontier models are trained on code, but they're trained on _descriptions_ of code, not necessarily on the full scope of their own capabilities in coding contexts. They can "forget" what tools they have access to. They can undersell their own abilities. + + This is why Rowl implements a **Guardian System**: a lightweight background process that watches what the AI is saying and gently reminds it of capabilities it might be forgetting. Not as a constraint, but as a memory. + +--- + +## Multi-Provider: The Fulfillment of Pluralsight for AI + +In traditional software, we don't ask "which cloud provider?" We use multiple providers because different tools excel at different things. AWS for infrastructure, Stripe for payments, Twilio for communications. + +**AI is the same.** + +- Claude excels at reasoning, nuance, and long-context tasks +- Codex excels at fast execution and GitHub integration +- Copilot excels at context-aware autocomplete +- OpenCode brings OpenRouter aggregation and unique MCP capabilities +- Kimi brings Chinese language and market capabilities +- Pi brings lightweight, fast iteration + +No single provider is "the best." The best setup is **the right provider for the right task**, which means you need a workspace that can: + +1. **Route tasks intelligently** to the most appropriate provider +2. **Maintain context across providers** when a task spans multiple +3. **Compare outputs** from different providers on the same problem +4. **Orchestrate multiple providers** in parallel on complex tasks + +Rowl is the fulfillment of this vision: a unified workspace where you can spin up any provider, any time, with full context preservation. + +--- + +## Multi-Project: Engineering at the Portfolio Level + +Professional engineers don't work on one project at a time. They maintain multiple projects, libraries, and systems simultaneously. They need to context-switch between projects without losing their mental model of each. + +**Current AI tools assume one project at a time.** Start a new chat, lose the old context. Work on Project A, have no idea what you were doing in Project B. + +Rowl's multi-project architecture treats your engineering portfolio as a first-class concept: + +- Each project has its own context, providers, and history +- Projects can share context when needed (shared libraries, monorepo structure) +- You can work on Project A, queue work on Project B, and monitor Project C—all in the same interface +- Thread history is preserved, searchable, and forkable across projects + +This is how actual engineering teams work. Why should AI tools pretend otherwise? + +--- + +## Simultaneous Agents: Parallel Intelligence + +The most powerful engineering teams don't have one engineer doing everything sequentially. They have multiple engineers working in parallel, with clear responsibilities, shared context, and communication protocols. + +**Rowl brings this to AI agents.** + +In Rowl, you can have: + +- An agent researching approach options +- An agent implementing core features +- An agent writing tests +- An agent reviewing the work + +All running simultaneously, all with access to the same project context, all coordinated through structured review and approval workflows. + +This isn't science fiction. This is what happens when you have proper: + +- **Context isolation** (agents don't step on each other's context) +- **Process isolation** (agents can crash without taking down the whole system) +- **Output structured review** (agent work goes into a review queue, not directly into your codebase) + +--- + +## Visibility: The Dashboard Principle + +When you fly a plane, you don't just trust the pilot's word that everything is fine. You have instruments that show altitude, speed, heading, fuel, engine status. The pilot has agency, but you can see what's happening. + +**Rowl applies the dashboard principle to AI agents.** + +In Rowl, you can see: + +- What each agent is currently working on +- How much context each agent is using +- What tools each agent has called +- What the agent is "thinking" (when reasoning is enabled) +- How far along each task is +- What approvals are pending + +This visibility does two things: + +1. **Builds trust** — you can see that the AI is actually working +2. **Enables intervention** — you can catch problems before they become disasters + +--- + +## Control: Context, Permissions, Workflows + +Visibility without control is just surveillance. Rowl gives you real control: + +### Context Control + +- **Surgical trimming** with tombstones (visible markers showing what was removed) +- **Context budgets** per agent, per project, per session +- **Selective context injection** from other threads, projects, or external sources +- **Context preservation** on interrupt/restart + +### Permission Control + +- **Allow/Ask/Deny policies** per tool category +- **Per-provider permission profiles** +- **Approval queues** for sensitive operations +- **Audit trails** of all approved/denied operations + +### Workflow Control + +- **Plan mode** for structured first-draft review +- **Apply/Review/Reject** workflows for agent outputs +- **Checkpointing** before and after major changes +- **Rollback** to any previous checkpoint + +--- + +## Content Mediums: Beyond Code + +Rowl starts with code because that's where AI-assisted engineering is most mature. But the vision extends beyond code. + +**Future Rowl:** + +### Video Workflow Canvas + +Inspired by Flashboards, Rowl will support video projects as first-class citizens: + +- AI-generated video content +- AI-assisted editing and composition +- Video assets in the same project as code +- Unified review and approval across media types + +### Image and Design Workflow + +With tools like Flora demonstrating AI image generation, Rowl will integrate: + +- Image generation and editing +- Design asset management +- Visual content alongside code deliverables +- Cross-medium context (e.g., "generate UI mockup, then implement it in code") + +### Document and Knowledge Work + +Technical documentation, architecture decision records, RFCs—all part of the engineering workflow. Rowl will treat these as first-class outputs, not afterthoughts. + +**The key insight:** Code, video, images, and documents are all _engineering outputs_. They should live in the same workspace, share context, and be subject to the same review and approval workflows. + +--- + +## Why This Matters: The Meta-Argument + +You might ask: "Why build all this? Aren't existing tools good enough?" + +The answer is **no, they are not good enough**, and here's why: + +### The Context Problem + +When AI tools run out of context, they don't gracefully degrade. They silently start forgetting things. You don't find out until the output is wrong, incomplete, or incoherent. This is a trust problem. + +### The Single-Provider Problem + +Different AI providers have different strengths. Locking into one provider means you're always using the second-best tool for some portion of your work. This is an efficiency problem. + +### The Visibility Problem + +Most AI tools give you output. They don't tell you _how_ they arrived at that output, _what_ they considered and rejected, or _what_ they're uncertain about. This is a reliability problem. + +### The Review Problem + +AI outputs go directly from generation to acceptance or rejection. There's no structured review, no comparison of alternatives, no audit trail. This is a quality problem. + +### The Medium Problem + +Code, video, images, documents—they're all engineering artifacts. Treating them as separate domains means context can't flow between them. This is an integration problem. + +**Rowl addresses all five problems.** It is not a better chatbot. It is a better _way of working with AI_. + +--- + +## The Name: Why "Rowl" + +A "rowl" is a low, rough, rumbling sound—like a cat purring, or the quiet satisfaction of work well done. + +Rowl captures the vibe: + +- **Relaxed** — You are in control, not the AI +- **Confident** — You can see what's happening and intervene when needed +- **Productive** — Multiple agents, parallel work, structured review + +Not "loud" like many AI tools that demand attention with notifications and urgency. **Quiet, steady, in control.** + +--- + +## Summary: What Rowl Is and Isn't + +**Rowl IS:** + +- A multi-provider AI orchestration workspace +- A multi-project context management system +- A simultaneous multi-agent coordination platform +- A visual control room with full visibility +- A structured review and approval workflow system +- A vibe-coded environment where humans notice and direct + +**Rowl IS NOT:** + +- A single-chat AI assistant +- A code-only tool (future: video, images, documents) +- A passive recipient of AI output +- A context-unaware automation system +- A one-provider solution + +--- + +## The North Star + +Rowl's north star is simple: + +> **Make AI-assisted engineering feel like conducting an orchestra, not operating a typewriter.** + +You have many instruments. You can play them simultaneously or in sequence. You can see all of them at once. You can adjust volume, tempo, and direction in real-time. You can save and revisit arrangements. You can collaborate with other conductors. + +This is what Rowl is building toward. + +--- + +_Version 1.0 — Initial Vision Document_ +_Last updated: Based on Valo transcript analysis and Rowl codebase review_ diff --git a/docs/release.md b/docs/release.md index 97a2b6383d5..68f4e4ce60d 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,6 +1,6 @@ # Release Checklist -This document covers how CUT3 desktop releases are built, optionally signed, and published from one tag. +This document covers how Rowl desktop releases are built, optionally signed, and published from one tag. ## Local desktop builds for your own machine @@ -70,11 +70,11 @@ Recommended local verification before sharing artifacts: - Publishes one GitHub Release with all produced files. - Versions with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases. - Only plain `X.Y.Z` releases are marked as the repository's latest release. - - Desktop prerelease artifacts launch as `CUT3`, the same as stable builds. - - The GitHub Release title is `CUT3 v`. + - Desktop prerelease artifacts launch as `Rowl`, the same as stable builds. + - The GitHub Release title is `Rowl v`. - Includes Electron auto-update metadata (`latest*.yml` and `*.blockmap`) in release assets. - Generates and verifies a `SHA256SUMS` manifest before publishing the GitHub Release. -- Optionally publishes the CLI package (`apps/server`, npm package `cut3`) when explicitly enabled. +- Optionally publishes the CLI package (`apps/server`, npm package `rowl`) when explicitly enabled. - Auto-enables signing when the required platform secrets are present. - Can fail the release instead of silently shipping unsigned macOS/Windows artifacts when signing is required. @@ -87,10 +87,10 @@ Recommended local verification before sharing artifacts: - The desktop UI shows a rocket update button when an update is available; click once to download, click again after download to restart/install. - Provider: GitHub Releases (`provider: github`) configured at build time. - Repository slug source: - - `CUT3_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set. + - `ROWL_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set. - otherwise `GITHUB_REPOSITORY` from GitHub Actions. - Temporary private-repo auth workaround: - - set `CUT3_DESKTOP_UPDATE_GITHUB_TOKEN` (or `GH_TOKEN`) in the desktop app runtime environment. + - set `ROWL_DESKTOP_UPDATE_GITHUB_TOKEN` (or `GH_TOKEN`) in the desktop app runtime environment. - the app forwards it as an `Authorization: Bearer ` request header for updater HTTP calls. - Required release assets for updater: - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads) @@ -105,13 +105,13 @@ Recommended local verification before sharing artifacts: The workflow only publishes the CLI when you explicitly opt in: - `workflow_dispatch` with `publish_cli=true`, or -- repository variable `CUT3_PUBLISH_CLI=true` for tag-triggered releases. +- repository variable `ROWL_PUBLISH_CLI=true` for tag-triggered releases. When enabled, it publishes the CLI with `bun publish` from `apps/server` after bumping the package version to the release tag version. Checklist: -1. Confirm npm org/user owns package `cut3`. +1. Confirm npm org/user owns package `rowl`. 2. In npm package settings, configure Trusted Publisher: - Provider: GitHub Actions - Repository: this repo @@ -155,7 +155,7 @@ Release signing is still auto-detected from secrets, but you can now make unsign Controls: - Tag-triggered releases: - - set repository variable `CUT3_REQUIRE_SIGNING=true` + - set repository variable `ROWL_REQUIRE_SIGNING=true` - Manual releases: - set workflow-dispatch input `require_signing=true` @@ -191,7 +191,7 @@ Checklist: - `APPLE_API_KEY_ID`: Key ID - `APPLE_API_ISSUER`: Issuer ID 8. Re-run a tag release and confirm macOS artifacts are signed and notarized. -9. If you want stable releases to fail when signing is unavailable, enable `CUT3_REQUIRE_SIGNING=true`. +9. If you want stable releases to fail when signing is unavailable, enable `ROWL_REQUIRE_SIGNING=true`. Notes: @@ -223,14 +223,14 @@ Checklist: 5. Create a client secret for the service principal. 6. Add the Azure secrets listed above in GitHub Actions secrets. 7. Re-run a tag release and confirm the Windows installer is signed. -8. If you want stable releases to fail when signing is unavailable, enable `CUT3_REQUIRE_SIGNING=true`. +8. If you want stable releases to fail when signing is unavailable, enable `ROWL_REQUIRE_SIGNING=true`. ## 5) Ongoing release checklist 1. Ensure `main` is green in CI. 2. Confirm the release version is correct. 3. Decide whether signing must be enforced for this run: - - repository variable `CUT3_REQUIRE_SIGNING=true`, or + - repository variable `ROWL_REQUIRE_SIGNING=true`, or - manual `require_signing=true` 4. Create release tag: `vX.Y.Z`. 5. Push tag. @@ -246,10 +246,10 @@ Checklist: - macOS build is unsigned when it should be signed: - check all Apple secrets are populated and non-empty - - enable `CUT3_REQUIRE_SIGNING=true` so the workflow fails instead of silently continuing unsigned + - enable `ROWL_REQUIRE_SIGNING=true` so the workflow fails instead of silently continuing unsigned - Windows build is unsigned when it should be signed: - check all Azure ATS and auth secrets are populated and non-empty - - enable `CUT3_REQUIRE_SIGNING=true` so the workflow fails instead of silently continuing unsigned + - enable `ROWL_REQUIRE_SIGNING=true` so the workflow fails instead of silently continuing unsigned - Build fails with signing verification errors: - retry with signing disabled to isolate packaging vs signing problems - re-check certificate/profile names and tenant/client credentials diff --git a/package.json b/package.json index 2632c039cc5..e37ea7a1341 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ "dev:web": "node scripts/dev-runner.ts dev:web", "dev:marketing": "turbo run dev --filter=@t3tools/marketing", "dev:desktop": "node scripts/dev-runner.ts dev:desktop", - "start": "turbo run start --filter=cut3", + "start": "turbo run start --filter=rowl", "start:desktop": "turbo run start --filter=@t3tools/desktop", "start:marketing": "turbo run preview --filter=@t3tools/marketing", "build": "turbo run build", "build:marketing": "turbo run build --filter=@t3tools/marketing", - "build:desktop": "turbo run build --filter=@t3tools/desktop --filter=cut3", + "build:desktop": "turbo run build --filter=@t3tools/desktop --filter=rowl", "typecheck": "turbo run typecheck", "lint": "oxlint --report-unused-disable-directives", "test": "turbo run test", diff --git a/packages/contracts/src/context.ts b/packages/contracts/src/context.ts new file mode 100644 index 00000000000..ed4e3e7f3bc --- /dev/null +++ b/packages/contracts/src/context.ts @@ -0,0 +1,108 @@ +import { Schema } from "effect"; + +import { + IsoDateTime, + NonNegativeInt, + ProjectId, + ThreadId, + TrimmedNonEmptyString, +} from "./baseSchemas"; + +export const ContextNodeType = Schema.Literals(["messages", "file", "artifact", "memory"]); +export type ContextNodeType = typeof ContextNodeType.Type; + +export const ContextNodeId = TrimmedNonEmptyString.pipe(Schema.brand("ContextNodeId")); +export type ContextNodeId = typeof ContextNodeId.Type; + +export const ContextNode = Schema.Struct({ + id: ContextNodeId, + projectId: ProjectId, + threadId: ThreadId, + type: ContextNodeType, + summary: TrimmedNonEmptyString, + size: NonNegativeInt, + compressed: Schema.Boolean, + createdAt: IsoDateTime, +}); +export type ContextNode = typeof ContextNode.Type; + +export const ContextBudget = Schema.Struct({ + total: NonNegativeInt, + used: NonNegativeInt, + available: NonNegativeInt, + compressionRatio: Schema.Number, +}); +export type ContextBudget = typeof ContextBudget.Type; + +export const GetContextNodeInput = Schema.Struct({ + id: ContextNodeId, +}); +export type GetContextNodeInput = typeof GetContextNodeInput.Type; + +export const GetContextNodeResult = Schema.Struct({ + node: ContextNode, +}); +export type GetContextNodeResult = typeof GetContextNodeResult.Type; + +export const ListContextNodesByProjectInput = Schema.Struct({ + projectId: ProjectId, +}); +export type ListContextNodesByProjectInput = typeof ListContextNodesByProjectInput.Type; + +export const ListContextNodesByProjectResult = Schema.Struct({ + nodes: Schema.Array(ContextNode), + budget: ContextBudget, +}); +export type ListContextNodesByProjectResult = typeof ListContextNodesByProjectResult.Type; + +export const ListContextNodesByThreadInput = Schema.Struct({ + threadId: ThreadId, +}); +export type ListContextNodesByThreadInput = typeof ListContextNodesByThreadInput.Type; + +export const ListContextNodesByThreadResult = Schema.Struct({ + nodes: Schema.Array(ContextNode), +}); +export type ListContextNodesByThreadResult = typeof ListContextNodesByThreadResult.Type; + +export const CompressContextNodeInput = Schema.Struct({ + id: ContextNodeId, +}); +export type CompressContextNodeInput = typeof CompressContextNodeInput.Type; + +export const CompressContextNodeResult = Schema.Struct({ + node: ContextNode, +}); +export type CompressContextNodeResult = typeof CompressContextNodeResult.Type; + +export const RestoreContextNodeInput = Schema.Struct({ + id: ContextNodeId, +}); +export type RestoreContextNodeInput = typeof RestoreContextNodeInput.Type; + +export const RestoreContextNodeResult = Schema.Struct({ + node: ContextNode, +}); +export type RestoreContextNodeResult = typeof RestoreContextNodeResult.Type; + +export const CreateContextNodeInput = Schema.Struct({ + projectId: ProjectId, + threadId: ThreadId, + type: ContextNodeType, + summary: TrimmedNonEmptyString, + size: NonNegativeInt, +}); +export type CreateContextNodeInput = typeof CreateContextNodeInput.Type; + +export const CreateContextNodeResult = Schema.Struct({ + node: ContextNode, +}); +export type CreateContextNodeResult = typeof CreateContextNodeResult.Type; + +export const DeleteContextNodeInput = Schema.Struct({ + id: ContextNodeId, +}); +export type DeleteContextNodeInput = typeof DeleteContextNodeInput.Type; + +export const DeleteContextNodeResult = Schema.Struct({}); +export type DeleteContextNodeResult = typeof DeleteContextNodeResult.Type; diff --git a/packages/contracts/src/features.ts b/packages/contracts/src/features.ts new file mode 100644 index 00000000000..4e2e5bd241c --- /dev/null +++ b/packages/contracts/src/features.ts @@ -0,0 +1,89 @@ +import { Schema } from "effect"; + +import { IsoDateTime, ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas"; + +export const FeatureStage = Schema.Literals(["backlog", "in_progress", "done", "wishlist"]); +export type FeatureStage = typeof FeatureStage.Type; + +export const FeatureId = TrimmedNonEmptyString.pipe(Schema.brand("FeatureId")); +export type FeatureId = typeof FeatureId.Type; + +export const Feature = Schema.Struct({ + id: FeatureId, + projectId: ProjectId, + name: TrimmedNonEmptyString, + description: Schema.String, + stage: FeatureStage, + threadId: Schema.optional(ThreadId), + createdAt: IsoDateTime, + updatedAt: IsoDateTime, + createdBy: Schema.Literals(["user", "pm"]), +}); +export type Feature = typeof Feature.Type; + +export const CreateFeatureInput = Schema.Struct({ + projectId: ProjectId, + name: TrimmedNonEmptyString, + description: Schema.String, + stage: FeatureStage, + threadId: Schema.optional(ThreadId), + createdBy: Schema.Literals(["user", "pm"]), +}); +export type CreateFeatureInput = typeof CreateFeatureInput.Type; + +export const CreateFeatureResult = Schema.Struct({ + feature: Feature, +}); +export type CreateFeatureResult = typeof CreateFeatureResult.Type; + +export const GetFeatureInput = Schema.Struct({ + id: FeatureId, +}); +export type GetFeatureInput = typeof GetFeatureInput.Type; + +export const GetFeatureResult = Schema.Struct({ + feature: Feature, +}); +export type GetFeatureResult = typeof GetFeatureResult.Type; + +export const ListFeaturesByProjectInput = Schema.Struct({ + projectId: ProjectId, +}); +export type ListFeaturesByProjectInput = typeof ListFeaturesByProjectInput.Type; + +export const ListFeaturesByProjectResult = Schema.Struct({ + features: Schema.Array(Feature), +}); +export type ListFeaturesByProjectResult = typeof ListFeaturesByProjectResult.Type; + +export const UpdateFeatureStageInput = Schema.Struct({ + id: FeatureId, + stage: FeatureStage, +}); +export type UpdateFeatureStageInput = typeof UpdateFeatureStageInput.Type; + +export const UpdateFeatureStageResult = Schema.Struct({ + feature: Feature, +}); +export type UpdateFeatureStageResult = typeof UpdateFeatureStageResult.Type; + +export const UpdateFeatureInput = Schema.Struct({ + id: FeatureId, + name: Schema.optional(TrimmedNonEmptyString), + description: Schema.optional(Schema.String), + threadId: Schema.optional(Schema.NullOr(ThreadId)), +}); +export type UpdateFeatureInput = typeof UpdateFeatureInput.Type; + +export const UpdateFeatureResult = Schema.Struct({ + feature: Feature, +}); +export type UpdateFeatureResult = typeof UpdateFeatureResult.Type; + +export const DeleteFeatureInput = Schema.Struct({ + id: FeatureId, +}); +export type DeleteFeatureInput = typeof DeleteFeatureInput.Type; + +export const DeleteFeatureResult = Schema.Struct({}); +export type DeleteFeatureResult = typeof DeleteFeatureResult.Type; diff --git a/packages/contracts/src/goals.ts b/packages/contracts/src/goals.ts new file mode 100644 index 00000000000..55722c5f248 --- /dev/null +++ b/packages/contracts/src/goals.ts @@ -0,0 +1,100 @@ +import { Schema } from "effect"; + +import { IsoDateTime, ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas"; + +export const GoalId = TrimmedNonEmptyString.pipe(Schema.brand("GoalId")); +export type GoalId = typeof GoalId.Type; + +export const Goal = Schema.Struct({ + id: GoalId, + projectId: ProjectId, + text: TrimmedNonEmptyString, + isMain: Schema.Boolean, + linkedThreadIds: Schema.Array(ThreadId), + createdAt: IsoDateTime, +}); +export type Goal = typeof Goal.Type; + +export const CreateGoalInput = Schema.Struct({ + projectId: ProjectId, + text: TrimmedNonEmptyString, + isMain: Schema.Boolean, +}); +export type CreateGoalInput = typeof CreateGoalInput.Type; + +export const CreateGoalResult = Schema.Struct({ + goal: Goal, +}); +export type CreateGoalResult = typeof CreateGoalResult.Type; + +export const GetGoalInput = Schema.Struct({ + id: GoalId, +}); +export type GetGoalInput = typeof GetGoalInput.Type; + +export const GetGoalResult = Schema.Struct({ + goal: Goal, +}); +export type GetGoalResult = typeof GetGoalResult.Type; + +export const ListGoalsByProjectInput = Schema.Struct({ + projectId: ProjectId, +}); +export type ListGoalsByProjectInput = typeof ListGoalsByProjectInput.Type; + +export const ListGoalsByProjectResult = Schema.Struct({ + goals: Schema.Array(Goal), +}); +export type ListGoalsByProjectResult = typeof ListGoalsByProjectResult.Type; + +export const SetMainGoalInput = Schema.Struct({ + id: GoalId, + isMain: Schema.Boolean, +}); +export type SetMainGoalInput = typeof SetMainGoalInput.Type; + +export const SetMainGoalResult = Schema.Struct({ + goal: Goal, +}); +export type SetMainGoalResult = typeof SetMainGoalResult.Type; + +export const LinkThreadToGoalInput = Schema.Struct({ + goalId: GoalId, + threadId: ThreadId, +}); +export type LinkThreadToGoalInput = typeof LinkThreadToGoalInput.Type; + +export const LinkThreadToGoalResult = Schema.Struct({ + goal: Goal, +}); +export type LinkThreadToGoalResult = typeof LinkThreadToGoalResult.Type; + +export const UnlinkThreadFromGoalInput = Schema.Struct({ + goalId: GoalId, + threadId: ThreadId, +}); +export type UnlinkThreadFromGoalInput = typeof UnlinkThreadFromGoalInput.Type; + +export const UnlinkThreadFromGoalResult = Schema.Struct({ + goal: Goal, +}); +export type UnlinkThreadFromGoalResult = typeof UnlinkThreadFromGoalResult.Type; + +export const UpdateGoalTextInput = Schema.Struct({ + id: GoalId, + text: TrimmedNonEmptyString, +}); +export type UpdateGoalTextInput = typeof UpdateGoalTextInput.Type; + +export const UpdateGoalTextResult = Schema.Struct({ + goal: Goal, +}); +export type UpdateGoalTextResult = typeof UpdateGoalTextResult.Type; + +export const DeleteGoalInput = Schema.Struct({ + id: GoalId, +}); +export type DeleteGoalInput = typeof DeleteGoalInput.Type; + +export const DeleteGoalResult = Schema.Struct({}); +export type DeleteGoalResult = typeof DeleteGoalResult.Type; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 985c787e966..af89df03879 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -12,3 +12,6 @@ export * from "./orchestration"; export * from "./editor"; export * from "./project"; export * from "./threadFeatures"; +export * from "./features"; +export * from "./goals"; +export * from "./context"; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index a8c8ac31972..83b7d96f173 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -31,6 +31,8 @@ import type { ProjectSearchEntriesResult, ProjectWriteFileInput, ProjectWriteFileResult, + ProjectDeleteFileInput, + ProjectDeleteFileResult, } from "./project"; import type { ServerConfig, @@ -176,6 +178,7 @@ export interface NativeApi { ) => Promise; listSkills: (input: ProjectListSkillsInput) => Promise; writeFile: (input: ProjectWriteFileInput) => Promise; + deleteFile: (input: ProjectDeleteFileInput) => Promise; }; threads: { getShareStatus: (input: ThreadShareStatusInput) => Promise; diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 71538f3713e..4d743a763f2 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -276,6 +276,7 @@ export const OrchestrationThread = Schema.Struct({ id: ThreadId, projectId: ProjectId, title: TrimmedNonEmptyString, + goal: Schema.NullOr(TrimmedNonEmptyString), model: TrimmedNonEmptyString, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode.pipe( @@ -388,6 +389,7 @@ const ThreadMetaUpdateCommand = Schema.Struct({ commandId: CommandId, threadId: ThreadId, title: Schema.optional(TrimmedNonEmptyString), + goal: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), model: Schema.optional(TrimmedNonEmptyString), branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), diff --git a/packages/contracts/src/project.ts b/packages/contracts/src/project.ts index f510907d85c..089a55dabc0 100644 --- a/packages/contracts/src/project.ts +++ b/packages/contracts/src/project.ts @@ -41,6 +41,15 @@ export const ProjectWriteFileResult = Schema.Struct({ }); export type ProjectWriteFileResult = typeof ProjectWriteFileResult.Type; +export const ProjectDeleteFileInput = Schema.Struct({ + cwd: TrimmedNonEmptyString, + relativePath: TrimmedNonEmptyString.check(Schema.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)), +}); +export type ProjectDeleteFileInput = typeof ProjectDeleteFileInput.Type; + +export const ProjectDeleteFileResult = Schema.Struct({}); +export type ProjectDeleteFileResult = typeof ProjectDeleteFileResult.Type; + const ProjectWorkspaceInput = Schema.Struct({ cwd: TrimmedNonEmptyString, }); diff --git a/packages/contracts/src/terminal.test.ts b/packages/contracts/src/terminal.test.ts index a224df5cfd7..4b5578798b4 100644 --- a/packages/contracts/src/terminal.test.ts +++ b/packages/contracts/src/terminal.test.ts @@ -66,12 +66,12 @@ describe("TerminalOpenInput", () => { cols: 100, rows: 24, env: { - CUT3_PROJECT_ROOT: "/tmp/project", + ROWL_PROJECT_ROOT: "/tmp/project", CUSTOM_FLAG: "1", }, }); expect(parsed.env).toMatchObject({ - CUT3_PROJECT_ROOT: "/tmp/project", + ROWL_PROJECT_ROOT: "/tmp/project", CUSTOM_FLAG: "1", }); }); diff --git a/packages/contracts/src/ws.ts b/packages/contracts/src/ws.ts index 2d2eca6cd9d..e7ff745a0a6 100644 --- a/packages/contracts/src/ws.ts +++ b/packages/contracts/src/ws.ts @@ -60,6 +60,8 @@ import { ProjectSearchEntriesResult, ProjectWriteFileInput, ProjectWriteFileResult, + ProjectDeleteFileInput, + ProjectDeleteFileResult, } from "./project"; import { ThreadCompactInput, @@ -95,6 +97,54 @@ import { ServerOpenCodeStateInput, ServerUpsertKeybindingResult, } from "./server"; +import { + CompressContextNodeInput, + CompressContextNodeResult, + CreateContextNodeInput, + CreateContextNodeResult, + DeleteContextNodeInput, + DeleteContextNodeResult, + GetContextNodeInput, + GetContextNodeResult, + ListContextNodesByProjectInput, + ListContextNodesByProjectResult, + ListContextNodesByThreadInput, + ListContextNodesByThreadResult, + RestoreContextNodeInput, + RestoreContextNodeResult, +} from "./context"; +import { + CreateFeatureInput, + CreateFeatureResult, + DeleteFeatureInput, + DeleteFeatureResult, + GetFeatureInput, + GetFeatureResult, + ListFeaturesByProjectInput, + ListFeaturesByProjectResult, + UpdateFeatureInput, + UpdateFeatureResult, + UpdateFeatureStageInput, + UpdateFeatureStageResult, +} from "./features"; +import { + CreateGoalInput, + CreateGoalResult, + DeleteGoalInput, + DeleteGoalResult, + GetGoalInput, + GetGoalResult, + LinkThreadToGoalInput, + LinkThreadToGoalResult, + ListGoalsByProjectInput, + ListGoalsByProjectResult, + SetMainGoalInput, + SetMainGoalResult, + UnlinkThreadFromGoalInput, + UnlinkThreadFromGoalResult, + UpdateGoalTextInput, + UpdateGoalTextResult, +} from "./goals"; // ── WebSocket RPC Method Names ─────────────────────────────────────── @@ -109,6 +159,7 @@ export const WS_METHODS = { projectsListCommandTemplates: "projects.listCommandTemplates", projectsListSkills: "projects.listSkills", projectsWriteFile: "projects.writeFile", + projectsDeleteFile: "projects.deleteFile", // Thread utility methods threadsGetShareStatus: "threads.getShareStatus", @@ -153,6 +204,33 @@ export const WS_METHODS = { serverUpsertKeybinding: "server.upsertKeybinding", serverAddOpenCodeCredential: "server.addOpenCodeCredential", serverRemoveOpenCodeCredential: "server.removeOpenCodeCredential", + + // Features methods + featuresCreate: "features.create", + featuresGet: "features.get", + featuresListByProject: "features.listByProject", + featuresUpdate: "features.update", + featuresUpdateStage: "features.updateStage", + featuresDelete: "features.delete", + + // Goals methods + goalsCreate: "goals.create", + goalsGet: "goals.get", + goalsListByProject: "goals.listByProject", + goalsSetMain: "goals.setMain", + goalsLinkThread: "goals.linkThread", + goalsUnlinkThread: "goals.unlinkThread", + goalsUpdateText: "goals.updateText", + goalsDelete: "goals.delete", + + // Context methods + contextCreateNode: "context.createNode", + contextGetNode: "context.getNode", + contextListByProject: "context.listByProject", + contextListByThread: "context.listByThread", + contextCompressNode: "context.compressNode", + contextRestoreNode: "context.restoreNode", + contextDeleteNode: "context.deleteNode", } as const; // ── Push Event Channels ────────────────────────────────────────────── @@ -193,6 +271,7 @@ const WebSocketRequestBody = Schema.Union([ tagRequestBody(WS_METHODS.projectsListCommandTemplates, ProjectListCommandTemplatesInput), tagRequestBody(WS_METHODS.projectsListSkills, ProjectListSkillsInput), tagRequestBody(WS_METHODS.projectsWriteFile, ProjectWriteFileInput), + tagRequestBody(WS_METHODS.projectsDeleteFile, ProjectDeleteFileInput), // Thread utility methods tagRequestBody(WS_METHODS.threadsGetShareStatus, ThreadShareStatusInput), @@ -237,6 +316,33 @@ const WebSocketRequestBody = Schema.Union([ tagRequestBody(WS_METHODS.serverUpsertKeybinding, KeybindingRule), tagRequestBody(WS_METHODS.serverAddOpenCodeCredential, ServerOpenCodeAddCredentialInput), tagRequestBody(WS_METHODS.serverRemoveOpenCodeCredential, ServerOpenCodeRemoveCredentialInput), + + // Features methods + tagRequestBody(WS_METHODS.featuresCreate, CreateFeatureInput), + tagRequestBody(WS_METHODS.featuresGet, GetFeatureInput), + tagRequestBody(WS_METHODS.featuresListByProject, ListFeaturesByProjectInput), + tagRequestBody(WS_METHODS.featuresUpdate, UpdateFeatureInput), + tagRequestBody(WS_METHODS.featuresUpdateStage, UpdateFeatureStageInput), + tagRequestBody(WS_METHODS.featuresDelete, DeleteFeatureInput), + + // Goals methods + tagRequestBody(WS_METHODS.goalsCreate, CreateGoalInput), + tagRequestBody(WS_METHODS.goalsGet, GetGoalInput), + tagRequestBody(WS_METHODS.goalsListByProject, ListGoalsByProjectInput), + tagRequestBody(WS_METHODS.goalsSetMain, SetMainGoalInput), + tagRequestBody(WS_METHODS.goalsLinkThread, LinkThreadToGoalInput), + tagRequestBody(WS_METHODS.goalsUnlinkThread, UnlinkThreadFromGoalInput), + tagRequestBody(WS_METHODS.goalsUpdateText, UpdateGoalTextInput), + tagRequestBody(WS_METHODS.goalsDelete, DeleteGoalInput), + + // Context methods + tagRequestBody(WS_METHODS.contextCreateNode, CreateContextNodeInput), + tagRequestBody(WS_METHODS.contextGetNode, GetContextNodeInput), + tagRequestBody(WS_METHODS.contextListByProject, ListContextNodesByProjectInput), + tagRequestBody(WS_METHODS.contextListByThread, ListContextNodesByThreadInput), + tagRequestBody(WS_METHODS.contextCompressNode, CompressContextNodeInput), + tagRequestBody(WS_METHODS.contextRestoreNode, RestoreContextNodeInput), + tagRequestBody(WS_METHODS.contextDeleteNode, DeleteContextNodeInput), ]); export const WebSocketRequest = Schema.Struct({ @@ -342,6 +448,7 @@ export const WsRpcResultSchemaByMethod = { [WS_METHODS.projectsListCommandTemplates]: ProjectListCommandTemplatesResult, [WS_METHODS.projectsListSkills]: ProjectListSkillsResult, [WS_METHODS.projectsWriteFile]: ProjectWriteFileResult, + [WS_METHODS.projectsDeleteFile]: ProjectDeleteFileResult, [WS_METHODS.threadsGetShareStatus]: ThreadShareStatusResult, [WS_METHODS.threadsCreateShare]: ThreadCreateShareResult, [WS_METHODS.threadsGetShare]: ThreadGetShareResult, @@ -376,6 +483,33 @@ export const WsRpcResultSchemaByMethod = { [WS_METHODS.serverUpsertKeybinding]: ServerUpsertKeybindingResult, [WS_METHODS.serverAddOpenCodeCredential]: ServerOpenCodeCredentialResult, [WS_METHODS.serverRemoveOpenCodeCredential]: ServerOpenCodeCredentialResult, + + // Features methods + [WS_METHODS.featuresCreate]: CreateFeatureResult, + [WS_METHODS.featuresGet]: GetFeatureResult, + [WS_METHODS.featuresListByProject]: ListFeaturesByProjectResult, + [WS_METHODS.featuresUpdate]: UpdateFeatureResult, + [WS_METHODS.featuresUpdateStage]: UpdateFeatureStageResult, + [WS_METHODS.featuresDelete]: DeleteFeatureResult, + + // Goals methods + [WS_METHODS.goalsCreate]: CreateGoalResult, + [WS_METHODS.goalsGet]: GetGoalResult, + [WS_METHODS.goalsListByProject]: ListGoalsByProjectResult, + [WS_METHODS.goalsSetMain]: SetMainGoalResult, + [WS_METHODS.goalsLinkThread]: LinkThreadToGoalResult, + [WS_METHODS.goalsUnlinkThread]: UnlinkThreadFromGoalResult, + [WS_METHODS.goalsUpdateText]: UpdateGoalTextResult, + [WS_METHODS.goalsDelete]: DeleteGoalResult, + + // Context methods + [WS_METHODS.contextCreateNode]: CreateContextNodeResult, + [WS_METHODS.contextGetNode]: GetContextNodeResult, + [WS_METHODS.contextListByProject]: ListContextNodesByProjectResult, + [WS_METHODS.contextListByThread]: ListContextNodesByThreadResult, + [WS_METHODS.contextCompressNode]: CompressContextNodeResult, + [WS_METHODS.contextRestoreNode]: RestoreContextNodeResult, + [WS_METHODS.contextDeleteNode]: DeleteContextNodeResult, } as const; export type WsRpcMethod = keyof typeof WsRpcResultSchemaByMethod; diff --git a/packages/shared/src/appRelease.test.ts b/packages/shared/src/appRelease.test.ts index 6e63230e6b1..1d4d46d8b90 100644 --- a/packages/shared/src/appRelease.test.ts +++ b/packages/shared/src/appRelease.test.ts @@ -38,47 +38,47 @@ describe("isForkPrereleaseVersion", () => { }); describe("resolveAppReleaseBranding", () => { - it("keeps local dev-server sessions on unified CUT3 branding", () => { + it("keeps local dev-server sessions on unified Rowl branding", () => { expect(resolveAppReleaseBranding({ version: "1.2.3", isDevelopment: true })).toEqual({ - stageLabel: "CUT3", - displayName: "CUT3", - productName: "CUT3", - appId: "com.t3tools.cut3", - stateDirName: "cut3", - userDataDirName: "cut3", + stageLabel: "Rowl", + displayName: "Rowl", + productName: "Rowl", + appId: "com.t3tools.rowl", + stateDirName: "rowl", + userDataDirName: "rowl", }); }); - it("keeps prerelease packages on unified CUT3 branding", () => { + it("keeps prerelease packages on unified Rowl branding", () => { expect(resolveAppReleaseBranding({ version: "0.0.11-alpha.3", isDevelopment: false })).toEqual({ - stageLabel: "CUT3", - displayName: "CUT3", - productName: "CUT3", - appId: "com.t3tools.cut3", - stateDirName: "cut3", - userDataDirName: "cut3", + stageLabel: "Rowl", + displayName: "Rowl", + productName: "Rowl", + appId: "com.t3tools.rowl", + stateDirName: "rowl", + userDataDirName: "rowl", }); }); - it("keeps fork prerelease packages on unified CUT3 branding", () => { + it("keeps fork prerelease packages on unified Rowl branding", () => { expect(resolveAppReleaseBranding({ version: "0.0.11-fork.3", isDevelopment: false })).toEqual({ - stageLabel: "CUT3", - displayName: "CUT3", - productName: "CUT3", - appId: "com.t3tools.cut3", - stateDirName: "cut3", - userDataDirName: "cut3", + stageLabel: "Rowl", + displayName: "Rowl", + productName: "Rowl", + appId: "com.t3tools.rowl", + stateDirName: "rowl", + userDataDirName: "rowl", }); }); - it("keeps stable packaged builds on unified CUT3 branding", () => { + it("keeps stable packaged builds on unified Rowl branding", () => { expect(resolveAppReleaseBranding({ version: "1.2.3", isDevelopment: false })).toEqual({ - stageLabel: "CUT3", - displayName: "CUT3", - productName: "CUT3", - appId: "com.t3tools.cut3", - stateDirName: "cut3", - userDataDirName: "cut3", + stageLabel: "Rowl", + displayName: "Rowl", + productName: "Rowl", + appId: "com.t3tools.rowl", + stateDirName: "rowl", + userDataDirName: "rowl", }); }); }); diff --git a/packages/shared/src/appRelease.ts b/packages/shared/src/appRelease.ts index 8bfdc5afa8b..0b1500f165d 100644 --- a/packages/shared/src/appRelease.ts +++ b/packages/shared/src/appRelease.ts @@ -1,9 +1,9 @@ const VERSION_PRERELEASE_PATTERN = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)$/; -const DEFAULT_DESKTOP_PRODUCT_NAME = "CUT3"; -const DEFAULT_DESKTOP_APP_ID = "com.t3tools.cut3"; -const DEFAULT_STAGE_LABEL = "CUT3"; -const DEFAULT_STATE_DIR_NAME = "cut3"; -const DEFAULT_USER_DATA_DIR_NAME = "cut3"; +const DEFAULT_DESKTOP_PRODUCT_NAME = "Rowl"; +const DEFAULT_DESKTOP_APP_ID = "com.t3tools.rowl"; +const DEFAULT_STAGE_LABEL = "Rowl"; +const DEFAULT_STATE_DIR_NAME = "rowl"; +const DEFAULT_USER_DATA_DIR_NAME = "rowl"; export interface AppReleaseBrandingInput { readonly version: string; @@ -11,7 +11,7 @@ export interface AppReleaseBrandingInput { } export interface AppReleaseBranding { - readonly stageLabel: "CUT3"; + readonly stageLabel: "Rowl"; readonly displayName: string; readonly productName: string; readonly appId: string; diff --git a/packages/shared/src/desktopBackend.ts b/packages/shared/src/desktopBackend.ts index 56b3670b69b..f297f7dfe45 100644 --- a/packages/shared/src/desktopBackend.ts +++ b/packages/shared/src/desktopBackend.ts @@ -1,4 +1,4 @@ -export const DESKTOP_BACKEND_READY_PREFIX = "[cut3-desktop-ready]"; +export const DESKTOP_BACKEND_READY_PREFIX = "[rowl-desktop-ready]"; export interface DesktopBackendReadyPayload { readonly port: number; diff --git a/packages/shared/src/shell.test.ts b/packages/shared/src/shell.test.ts index f9ec3f790a5..3505ed7ea89 100644 --- a/packages/shared/src/shell.test.ts +++ b/packages/shared/src/shell.test.ts @@ -6,7 +6,7 @@ describe("extractPathFromShellOutput", () => { it("extracts the path between capture markers", () => { expect( extractPathFromShellOutput( - "__CUT3_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__CUT3_PATH_END__\n", + "__ROWL_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__ROWL_PATH_END__\n", ), ).toBe("/opt/homebrew/bin:/usr/bin"); }); @@ -14,7 +14,7 @@ describe("extractPathFromShellOutput", () => { it("ignores shell startup noise around the capture markers", () => { expect( extractPathFromShellOutput( - "Welcome to fish\n__CUT3_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__CUT3_PATH_END__\nBye\n", + "Welcome to fish\n__ROWL_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__ROWL_PATH_END__\nBye\n", ), ).toBe("/opt/homebrew/bin:/usr/bin"); }); @@ -32,7 +32,7 @@ describe("readPathFromLoginShell", () => { args: ReadonlyArray, options: { encoding: "utf8"; timeout: number }, ) => string - >(() => "__CUT3_PATH_START__\n/a:/b\n__CUT3_PATH_END__\n"); + >(() => "__ROWL_PATH_START__\n/a:/b\n__ROWL_PATH_END__\n"); expect(readPathFromLoginShell("/opt/homebrew/bin/fish", execFile)).toBe("/a:/b"); expect(execFile).toHaveBeenCalledTimes(1); @@ -50,8 +50,8 @@ describe("readPathFromLoginShell", () => { expect(args).toHaveLength(2); expect(args?.[0]).toBe("-ilc"); expect(args?.[1]).toContain("printenv PATH"); - expect(args?.[1]).toContain("__CUT3_PATH_START__"); - expect(args?.[1]).toContain("__CUT3_PATH_END__"); + expect(args?.[1]).toContain("__ROWL_PATH_START__"); + expect(args?.[1]).toContain("__ROWL_PATH_END__"); expect(options).toEqual({ encoding: "utf8", timeout: 5000 }); }); }); diff --git a/packages/shared/src/shell.ts b/packages/shared/src/shell.ts index 08c3c9d0a20..4916ddc9e22 100644 --- a/packages/shared/src/shell.ts +++ b/packages/shared/src/shell.ts @@ -1,7 +1,7 @@ import { execFileSync } from "node:child_process"; -const PATH_CAPTURE_START = "__CUT3_PATH_START__"; -const PATH_CAPTURE_END = "__CUT3_PATH_END__"; +const PATH_CAPTURE_START = "__ROWL_PATH_START__"; +const PATH_CAPTURE_END = "__ROWL_PATH_END__"; const PATH_CAPTURE_COMMAND = [ `printf '%s\n' '${PATH_CAPTURE_START}'`, "printenv PATH", diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 5d696f9fff2..baa3ee9dc3e 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -169,7 +169,7 @@ interface StagePackageJson { readonly name: string; readonly version: string; readonly buildVersion: string; - readonly cut3CommitHash: string; + readonly rowlCommitHash: string; readonly private: true; readonly description: string; readonly author: string; @@ -196,15 +196,15 @@ const AzureTrustedSigningOptionsConfig = Config.all({ }); const BuildEnvConfig = Config.all({ - platform: Config.schema(BuildPlatform, "CUT3_DESKTOP_PLATFORM").pipe(Config.option), - target: Config.string("CUT3_DESKTOP_TARGET").pipe(Config.option), - arch: Config.schema(BuildArch, "CUT3_DESKTOP_ARCH").pipe(Config.option), - version: Config.string("CUT3_DESKTOP_VERSION").pipe(Config.option), - outputDir: Config.string("CUT3_DESKTOP_OUTPUT_DIR").pipe(Config.option), - skipBuild: Config.boolean("CUT3_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), - keepStage: Config.boolean("CUT3_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), - signed: Config.boolean("CUT3_DESKTOP_SIGNED").pipe(Config.withDefault(false)), - verbose: Config.boolean("CUT3_DESKTOP_VERBOSE").pipe(Config.withDefault(false)), + platform: Config.schema(BuildPlatform, "ROWL_DESKTOP_PLATFORM").pipe(Config.option), + target: Config.string("ROWL_DESKTOP_TARGET").pipe(Config.option), + arch: Config.schema(BuildArch, "ROWL_DESKTOP_ARCH").pipe(Config.option), + version: Config.string("ROWL_DESKTOP_VERSION").pipe(Config.option), + outputDir: Config.string("ROWL_DESKTOP_OUTPUT_DIR").pipe(Config.option), + skipBuild: Config.boolean("ROWL_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), + keepStage: Config.boolean("ROWL_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), + signed: Config.boolean("ROWL_DESKTOP_SIGNED").pipe(Config.withDefault(false)), + verbose: Config.boolean("ROWL_DESKTOP_VERBOSE").pipe(Config.withDefault(false)), }); const resolveBooleanFlag = (flag: Option.Option, envValue: boolean) => @@ -318,7 +318,7 @@ function stageMacIcons(stageResourcesDir: string, verbose: boolean) { } const tmpRoot = yield* fs.makeTempDirectoryScoped({ - prefix: "cut3-icon-build-", + prefix: "rowl-icon-build-", }); const iconPngPath = path.join(stageResourcesDir, "icon.png"); @@ -427,7 +427,7 @@ function resolveGitHubPublishConfig(): } | undefined { const rawRepo = - process.env.CUT3_DESKTOP_UPDATE_REPOSITORY?.trim() || + process.env.ROWL_DESKTOP_UPDATE_REPOSITORY?.trim() || process.env.GITHUB_REPOSITORY?.trim() || ""; if (!rawRepo) return undefined; @@ -452,10 +452,10 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( ) { const artifactName = platform === "mac" - ? "CUT3-macOS-${version}-${arch}.${ext}" + ? "Rowl-macOS-${version}-${arch}.${ext}" : platform === "linux" - ? "CUT3-linux-${version}-${arch}.${ext}" - : "CUT3-windows-${version}-${arch}.${ext}"; + ? "Rowl-linux-${version}-${arch}.${ext}" + : "Rowl-windows-${version}-${arch}.${ext}"; const buildConfig: Record = { appId, productName, @@ -482,7 +482,7 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( target: [target], icon: "icon.png", category: "Development", - executableName: "cut3", + executableName: "rowl", desktop: { entry: { StartupWMClass: productName, @@ -583,7 +583,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const commitHash = resolveGitCommitHash(repoRoot); const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; const stageRoot = yield* mkdir({ - prefix: `cut3-desktop-${options.platform}-stage-`, + prefix: `rowl-desktop-${options.platform}-stage-`, }); const stageAppDir = path.join(stageRoot, "app"); @@ -637,12 +637,12 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( yield* fs.copy(stageResourcesDir, path.join(stageAppDir, "apps/desktop/prod-resources")); const stagePackageJson: StagePackageJson = { - name: "cut3-desktop", + name: "rowl-desktop", version: appVersion, buildVersion: appVersion, - cut3CommitHash: commitHash, + rowlCommitHash: commitHash, private: true, - description: "CUT3 desktop build", + description: "Rowl desktop build", author: "T3 Tools", main: "apps/desktop/dist-electron/main.js", build: yield* createBuildConfig( @@ -748,49 +748,49 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { platform: Flag.choice("platform", BuildPlatform.literals).pipe( - Flag.withDescription("Build platform (env: CUT3_DESKTOP_PLATFORM)."), + Flag.withDescription("Build platform (env: ROWL_DESKTOP_PLATFORM)."), Flag.optional, ), target: Flag.string("target").pipe( Flag.withDescription( - "Artifact target, for example dmg/AppImage/nsis (env: CUT3_DESKTOP_TARGET).", + "Artifact target, for example dmg/AppImage/nsis (env: ROWL_DESKTOP_TARGET).", ), Flag.optional, ), arch: Flag.choice("arch", BuildArch.literals).pipe( - Flag.withDescription("Build arch, for example arm64/x64/universal (env: CUT3_DESKTOP_ARCH)."), + Flag.withDescription("Build arch, for example arm64/x64/universal (env: ROWL_DESKTOP_ARCH)."), Flag.optional, ), buildVersion: Flag.string("build-version").pipe( - Flag.withDescription("Artifact version metadata (env: CUT3_DESKTOP_VERSION)."), + Flag.withDescription("Artifact version metadata (env: ROWL_DESKTOP_VERSION)."), Flag.optional, ), outputDir: Flag.string("output-dir").pipe( - Flag.withDescription("Output directory for artifacts (env: CUT3_DESKTOP_OUTPUT_DIR)."), + Flag.withDescription("Output directory for artifacts (env: ROWL_DESKTOP_OUTPUT_DIR)."), Flag.optional, ), skipBuild: Flag.boolean("skip-build").pipe( Flag.withDescription( - "Skip `bun run build:desktop` and use existing dist artifacts (env: CUT3_DESKTOP_SKIP_BUILD).", + "Skip `bun run build:desktop` and use existing dist artifacts (env: ROWL_DESKTOP_SKIP_BUILD).", ), Flag.optional, ), keepStage: Flag.boolean("keep-stage").pipe( - Flag.withDescription("Keep temporary staging files (env: CUT3_DESKTOP_KEEP_STAGE)."), + Flag.withDescription("Keep temporary staging files (env: ROWL_DESKTOP_KEEP_STAGE)."), Flag.optional, ), signed: Flag.boolean("signed").pipe( Flag.withDescription( - "Enable signing/notarization discovery; Windows uses Azure Trusted Signing (env: CUT3_DESKTOP_SIGNED).", + "Enable signing/notarization discovery; Windows uses Azure Trusted Signing (env: ROWL_DESKTOP_SIGNED).", ), Flag.optional, ), verbose: Flag.boolean("verbose").pipe( - Flag.withDescription("Stream subprocess stdout (env: CUT3_DESKTOP_VERBOSE)."), + Flag.withDescription("Stream subprocess stdout (env: ROWL_DESKTOP_VERBOSE)."), Flag.optional, ), }).pipe( - Command.withDescription("Build a desktop artifact for CUT3."), + Command.withDescription("Build a desktop artifact for Rowl."), Command.withHandler((input) => Effect.flatMap(resolveBuildOptions(input), buildDesktopArtifact)), ); diff --git a/scripts/create-release-checksums.test.ts b/scripts/create-release-checksums.test.ts index e86c76a0754..a45aafd10d7 100644 --- a/scripts/create-release-checksums.test.ts +++ b/scripts/create-release-checksums.test.ts @@ -12,25 +12,25 @@ import { describe("create-release-checksums", () => { it("writes stable SHA256SUMS entries for release assets", () => { - const rootDir = mkdtempSync(join(tmpdir(), "cut3-release-checksums-")); + const rootDir = mkdtempSync(join(tmpdir(), "rowl-release-checksums-")); try { - writeFileSync(resolve(rootDir, "CUT3-linux-1.0.0-x86_64.AppImage"), "linux-app"); + writeFileSync(resolve(rootDir, "Rowl-linux-1.0.0-x86_64.AppImage"), "linux-app"); writeFileSync(resolve(rootDir, "latest.yml"), "channel-manifest"); mkdirSync(resolve(rootDir, "nested"), { recursive: true }); - writeFileSync(resolve(rootDir, "nested", "CUT3-macOS-1.0.0-arm64.zip"), "mac-zip"); + writeFileSync(resolve(rootDir, "nested", "Rowl-macOS-1.0.0-arm64.zip"), "mac-zip"); writeFileSync(resolve(rootDir, "SHA256SUMS"), "stale-manifest"); const entries = createReleaseChecksums(rootDir); assert.deepStrictEqual( entries.map((entry) => entry.path), - ["CUT3-linux-1.0.0-x86_64.AppImage", "latest.yml", "nested/CUT3-macOS-1.0.0-arm64.zip"], + ["latest.yml", "nested/Rowl-macOS-1.0.0-arm64.zip", "Rowl-linux-1.0.0-x86_64.AppImage"], ); const serialized = serializeReleaseChecksums(entries); assert.equal(serialized.split("\n").length, 3); - assert.ok(serialized.includes("CUT3-linux-1.0.0-x86_64.AppImage")); - assert.ok(serialized.includes("nested/CUT3-macOS-1.0.0-arm64.zip")); + assert.ok(serialized.includes("Rowl-linux-1.0.0-x86_64.AppImage")); + assert.ok(serialized.includes("nested/Rowl-macOS-1.0.0-arm64.zip")); const outputPath = writeReleaseChecksums(rootDir); const written = readFileSync(outputPath, "utf8"); diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index a4432235d9a..507c7845d66 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -13,12 +13,12 @@ import { it.layer(NodeServices.layer)("dev-runner", (it) => { describe("resolveOffset", () => { - it.effect("uses explicit CUT3_PORT_OFFSET when provided", () => + it.effect("uses explicit ROWL_PORT_OFFSET when provided", () => Effect.sync(() => { const result = resolveOffset({ portOffset: 12, devInstance: undefined }); assert.deepStrictEqual(result, { offset: 12, - source: "CUT3_PORT_OFFSET=12", + source: "ROWL_PORT_OFFSET=12", }); }), ); @@ -40,7 +40,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { }), ); - assert.ok(error.includes("Invalid CUT3_PORT_OFFSET")); + assert.ok(error.includes("Invalid ROWL_PORT_OFFSET")); }), ); }); @@ -66,7 +66,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { DEFAULT_DEV_STATE_DIR, ]); - assert.equal(env.CUT3_STATE_DIR, defaultStateDir); + assert.equal(env.ROWL_STATE_DIR, defaultStateDir); }), ); @@ -87,13 +87,13 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { devUrl: new URL("http://localhost:7331"), }); - assert.equal(env.CUT3_STATE_DIR, resolve("/tmp/override-state")); - assert.equal(env.CUT3_PORT, "4222"); + assert.equal(env.ROWL_STATE_DIR, resolve("/tmp/override-state")); + assert.equal(env.ROWL_PORT, "4222"); assert.equal(env.VITE_WS_URL, "ws://127.0.0.1:4222/?token=secret"); - assert.equal(env.CUT3_NO_BROWSER, "1"); - assert.equal(env.CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD, "0"); - assert.equal(env.CUT3_LOG_WS_EVENTS, "1"); - assert.equal(env.CUT3_HOST, "0.0.0.0"); + assert.equal(env.ROWL_NO_BROWSER, "1"); + assert.equal(env.ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD, "0"); + assert.equal(env.ROWL_LOG_WS_EVENTS, "1"); + assert.equal(env.ROWL_HOST, "0.0.0.0"); assert.equal(env.VITE_DEV_SERVER_URL, "http://localhost:7331/"); }), ); @@ -103,7 +103,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: { - CUT3_LOG_WS_EVENTS: "keep-me-out", + ROWL_LOG_WS_EVENTS: "keep-me-out", }, serverOffset: 0, webOffset: 0, @@ -117,8 +117,8 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { devUrl: undefined, }); - assert.equal(env.CUT3_MODE, "web"); - assert.equal(env.CUT3_LOG_WS_EVENTS, undefined); + assert.equal(env.ROWL_MODE, "web"); + assert.equal(env.ROWL_LOG_WS_EVENTS, undefined); }), ); @@ -139,7 +139,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { devUrl: undefined, }); - assert.equal(env.CUT3_LOG_WS_EVENTS, "0"); + assert.equal(env.ROWL_LOG_WS_EVENTS, "0"); }), ); }); diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index 4a6a2920248..5a33a206ed1 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -30,10 +30,10 @@ const MODE_ARGS = { "--ui=tui", "--filter=@t3tools/contracts", "--filter=@t3tools/web", - "--filter=cut3", + "--filter=rowl", "--parallel", ], - "dev:server": ["run", "dev", "--filter=cut3"], + "dev:server": ["run", "dev", "--filter=rowl"], "dev:web": ["run", "dev", "--filter=@t3tools/web"], "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"], } as const satisfies Record>; @@ -86,8 +86,8 @@ const optionalUrlConfig = (name: string): Config.Config => ); const OffsetConfig = Config.all({ - portOffset: optionalIntegerConfig("CUT3_PORT_OFFSET"), - devInstance: optionalStringConfig("CUT3_DEV_INSTANCE"), + portOffset: optionalIntegerConfig("ROWL_PORT_OFFSET"), + devInstance: optionalStringConfig("ROWL_DEV_INSTANCE"), }); export function resolveOffset(config: { @@ -96,11 +96,11 @@ export function resolveOffset(config: { }): { readonly offset: number; readonly source: string } { if (config.portOffset !== undefined) { if (config.portOffset < 0) { - throw new Error(`Invalid CUT3_PORT_OFFSET: ${config.portOffset}`); + throw new Error(`Invalid ROWL_PORT_OFFSET: ${config.portOffset}`); } return { offset: config.portOffset, - source: `CUT3_PORT_OFFSET=${config.portOffset}`, + source: `ROWL_PORT_OFFSET=${config.portOffset}`, }; } @@ -110,11 +110,11 @@ export function resolveOffset(config: { } if (/^\d+$/.test(seed)) { - return { offset: Number(seed), source: `numeric CUT3_DEV_INSTANCE=${seed}` }; + return { offset: Number(seed), source: `numeric ROWL_DEV_INSTANCE=${seed}` }; } const offset = ((Hash.string(seed) >>> 0) % MAX_HASH_OFFSET) + 1; - return { offset, source: `hashed CUT3_DEV_INSTANCE=${seed}` }; + return { offset, source: `hashed ROWL_DEV_INSTANCE=${seed}` }; } function resolveStateDir(stateDir: string | undefined): Effect.Effect { @@ -288,50 +288,50 @@ export function createDevRunnerEnv({ const output: NodeJS.ProcessEnv = { ...baseEnv, - CUT3_PORT: String(serverPort), + ROWL_PORT: String(serverPort), PORT: String(webPort), ELECTRON_RENDERER_PORT: String(webPort), VITE_WS_URL: withWsAuthToken(`ws://${urlHost}:${serverPort}`, authToken), VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://${urlHost}:${webPort}`, - CUT3_STATE_DIR: resolvedStateDir, + ROWL_STATE_DIR: resolvedStateDir, }; if (host !== undefined) { - output.CUT3_HOST = host; + output.ROWL_HOST = host; } if (authToken !== undefined) { - output.CUT3_AUTH_TOKEN = authToken; + output.ROWL_AUTH_TOKEN = authToken; } else { - delete output.CUT3_AUTH_TOKEN; + delete output.ROWL_AUTH_TOKEN; } if (noBrowser !== undefined) { - output.CUT3_NO_BROWSER = noBrowser ? "1" : "0"; + output.ROWL_NO_BROWSER = noBrowser ? "1" : "0"; } else { - delete output.CUT3_NO_BROWSER; + delete output.ROWL_NO_BROWSER; } if (autoBootstrapProjectFromCwd !== undefined) { - output.CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = autoBootstrapProjectFromCwd ? "1" : "0"; + output.ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = autoBootstrapProjectFromCwd ? "1" : "0"; } else { - delete output.CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD; + delete output.ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD; } if (logWebSocketEvents !== undefined) { - output.CUT3_LOG_WS_EVENTS = logWebSocketEvents ? "1" : "0"; + output.ROWL_LOG_WS_EVENTS = logWebSocketEvents ? "1" : "0"; } else { - delete output.CUT3_LOG_WS_EVENTS; + delete output.ROWL_LOG_WS_EVENTS; } if (mode === "dev") { - output.CUT3_MODE = "web"; - delete output.CUT3_DESKTOP_WS_URL; + output.ROWL_MODE = "web"; + delete output.ROWL_DESKTOP_WS_URL; } if (mode === "dev:server" || mode === "dev:web") { - output.CUT3_MODE = "web"; - delete output.CUT3_DESKTOP_WS_URL; + output.ROWL_MODE = "web"; + delete output.ROWL_DESKTOP_WS_URL; } return output; @@ -525,7 +525,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { Effect.mapError( (cause) => new DevRunnerError({ - message: "Failed to read CUT3_PORT_OFFSET/CUT3_DEV_INSTANCE configuration.", + message: "Failed to read ROWL_PORT_OFFSET/ROWL_DEV_INSTANCE configuration.", cause, }), ), @@ -541,9 +541,9 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { }); const envOverrides = { - noBrowser: readOptionalBooleanEnv("CUT3_NO_BROWSER"), - autoBootstrapProjectFromCwd: readOptionalBooleanEnv("CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD"), - logWebSocketEvents: readOptionalBooleanEnv("CUT3_LOG_WS_EVENTS"), + noBrowser: readOptionalBooleanEnv("ROWL_NO_BROWSER"), + autoBootstrapProjectFromCwd: readOptionalBooleanEnv("ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD"), + logWebSocketEvents: readOptionalBooleanEnv("ROWL_LOG_WS_EVENTS"), }; const resolvedStateDir = yield* resolveStateDir(input.stateDir); @@ -609,7 +609,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { : ""; yield* Effect.logInfo( - `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.CUT3_PORT)} webPort=${String(env.PORT)} stateDir=${String(env.CUT3_STATE_DIR)}`, + `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.ROWL_PORT)} webPort=${String(env.PORT)} stateDir=${String(env.ROWL_STATE_DIR)}`, ); if (input.dryRun) { @@ -658,37 +658,37 @@ const devRunnerCli = Command.make("dev-runner", { Argument.withDescription("Development mode to run."), ), stateDir: Flag.string("state-dir").pipe( - Flag.withDescription("State directory path (forwards to CUT3_STATE_DIR)."), - Flag.withFallbackConfig(optionalStringConfig("CUT3_STATE_DIR")), + Flag.withDescription("State directory path (forwards to ROWL_STATE_DIR)."), + Flag.withFallbackConfig(optionalStringConfig("ROWL_STATE_DIR")), ), authToken: Flag.string("auth-token").pipe( - Flag.withDescription("Auth token (forwards to CUT3_AUTH_TOKEN)."), + Flag.withDescription("Auth token (forwards to ROWL_AUTH_TOKEN)."), Flag.withAlias("token"), - Flag.withFallbackConfig(optionalStringConfig("CUT3_AUTH_TOKEN")), + Flag.withFallbackConfig(optionalStringConfig("ROWL_AUTH_TOKEN")), ), noBrowser: Flag.boolean("no-browser").pipe( - Flag.withDescription("Browser auto-open toggle (equivalent to CUT3_NO_BROWSER)."), - Flag.withFallbackConfig(optionalBooleanConfig("CUT3_NO_BROWSER")), + Flag.withDescription("Browser auto-open toggle (equivalent to ROWL_NO_BROWSER)."), + Flag.withFallbackConfig(optionalBooleanConfig("ROWL_NO_BROWSER")), ), autoBootstrapProjectFromCwd: Flag.boolean("auto-bootstrap-project-from-cwd").pipe( Flag.withDescription( - "Auto-bootstrap toggle (equivalent to CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD).", + "Auto-bootstrap toggle (equivalent to ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD).", ), - Flag.withFallbackConfig(optionalBooleanConfig("CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD")), + Flag.withFallbackConfig(optionalBooleanConfig("ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD")), ), logWebSocketEvents: Flag.boolean("log-websocket-events").pipe( - Flag.withDescription("WebSocket event logging toggle (equivalent to CUT3_LOG_WS_EVENTS)."), + Flag.withDescription("WebSocket event logging toggle (equivalent to ROWL_LOG_WS_EVENTS)."), Flag.withAlias("log-ws-events"), - Flag.withFallbackConfig(optionalBooleanConfig("CUT3_LOG_WS_EVENTS")), + Flag.withFallbackConfig(optionalBooleanConfig("ROWL_LOG_WS_EVENTS")), ), host: Flag.string("host").pipe( - Flag.withDescription("Server host/interface override (forwards to CUT3_HOST)."), - Flag.withFallbackConfig(optionalStringConfig("CUT3_HOST")), + Flag.withDescription("Server host/interface override (forwards to ROWL_HOST)."), + Flag.withFallbackConfig(optionalStringConfig("ROWL_HOST")), ), port: Flag.integer("port").pipe( Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), - Flag.withDescription("Server port override (forwards to CUT3_PORT)."), - Flag.withFallbackConfig(optionalPortConfig("CUT3_PORT")), + Flag.withDescription("Server port override (forwards to ROWL_PORT)."), + Flag.withFallbackConfig(optionalPortConfig("ROWL_PORT")), ), devUrl: Flag.string("dev-url").pipe( Flag.withSchema(Schema.URLFromString), diff --git a/scripts/merge-mac-update-manifests.compat.test.ts b/scripts/merge-mac-update-manifests.compat.test.ts index a891f92532a..727dfcb8cd7 100644 --- a/scripts/merge-mac-update-manifests.compat.test.ts +++ b/scripts/merge-mac-update-manifests.compat.test.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { mergeMacUpdateManifests, @@ -12,13 +12,13 @@ function makeMergedMacManifestYaml(): string { const arm64 = parseMacUpdateManifest( `version: 0.0.12-fork.2 files: - - url: CUT3-macOS-0.0.12-fork.2-arm64.zip + - url: Rowl-macOS-0.0.12-fork.2-arm64.zip sha512: arm64zip size: 10 - - url: CUT3-macOS-0.0.12-fork.2-arm64.dmg + - url: Rowl-macOS-0.0.12-fork.2-arm64.dmg sha512: arm64dmg size: 11 -path: CUT3-macOS-0.0.12-fork.2-arm64.zip +path: Rowl-macOS-0.0.12-fork.2-arm64.zip sha512: arm64zip releaseDate: '2026-03-12T23:30:00.000Z' `, @@ -28,13 +28,13 @@ releaseDate: '2026-03-12T23:30:00.000Z' const x64 = parseMacUpdateManifest( `version: 0.0.12-fork.2 files: - - url: CUT3-macOS-0.0.12-fork.2-x64.zip + - url: Rowl-macOS-0.0.12-fork.2-x64.zip sha512: x64zip size: 12 - - url: CUT3-macOS-0.0.12-fork.2-x64.dmg + - url: Rowl-macOS-0.0.12-fork.2-x64.dmg sha512: x64dmg size: 13 -path: CUT3-macOS-0.0.12-fork.2-x64.zip +path: Rowl-macOS-0.0.12-fork.2-x64.zip sha512: x64zip releaseDate: '2026-03-12T23:31:00.000Z' `, @@ -128,6 +128,11 @@ function makeFakeMacUpdater(MacUpdater: ElectronUpdaterModules["MacUpdater"]): a } async function selectMacUpdateFile(args: { uname: string; rosetta: boolean }): Promise { + mockedArch = args.uname.toLowerCase().includes("arm") ? "arm64" : "x64"; + Object.defineProperty(process, "arch", { + get: () => mockedArch ?? originalProcessArch, + }); + const modules = await loadElectronUpdaterModules(args); const updater = makeFakeMacUpdater(modules.MacUpdater); @@ -142,27 +147,37 @@ async function selectMacUpdateFile(args: { uname: string; rosetta: boolean }): P return call?.fileInfo.url.pathname ?? ""; } -afterEach(() => { - childProcessModule.execFileSync = originalExecFileSync; - vi.restoreAllMocks(); -}); +const originalProcessArch = process.arch; +let mockedArch: string | undefined; describe("merge-mac-update-manifests electron-updater compatibility", () => { + beforeEach(() => { + mockedArch = undefined; + }); + + afterEach(() => { + childProcessModule.execFileSync = originalExecFileSync; + Object.defineProperty(process, "arch", { + get: () => mockedArch ?? originalProcessArch, + }); + vi.restoreAllMocks(); + }); + it("selects the x64 zip for x64 macOS hosts", async () => { await expect( selectMacUpdateFile({ uname: "Darwin x86_64 Apple Kernel Version", rosetta: false }), - ).resolves.toContain("CUT3-macOS-0.0.12-fork.2-x64.zip"); + ).resolves.toContain("Rowl-macOS-0.0.12-fork.2-x64.zip"); }); it("selects the arm64 zip for native arm64 macOS hosts", async () => { await expect( selectMacUpdateFile({ uname: "Darwin ARM64 Apple Kernel Version", rosetta: false }), - ).resolves.toContain("CUT3-macOS-0.0.12-fork.2-arm64.zip"); + ).resolves.toContain("Rowl-macOS-0.0.12-fork.2-arm64.zip"); }); it("selects the arm64 zip for Rosetta-translated macOS hosts", async () => { await expect( selectMacUpdateFile({ uname: "Darwin x86_64 Apple Kernel Version", rosetta: true }), - ).resolves.toContain("CUT3-macOS-0.0.12-fork.2-arm64.zip"); + ).resolves.toContain("Rowl-macOS-0.0.12-fork.2-arm64.zip"); }); }); diff --git a/scripts/merge-mac-update-manifests.test.ts b/scripts/merge-mac-update-manifests.test.ts index 71a5545cf9a..925d2ccb866 100644 --- a/scripts/merge-mac-update-manifests.test.ts +++ b/scripts/merge-mac-update-manifests.test.ts @@ -11,13 +11,13 @@ describe("merge-mac-update-manifests", () => { const arm64 = parseMacUpdateManifest( `version: 0.0.4 files: - - url: CUT3-macOS-0.0.4-arm64.zip + - url: Rowl-macOS-0.0.4-arm64.zip sha512: arm64zip size: 125621344 - - url: CUT3-macOS-0.0.4-arm64.dmg + - url: Rowl-macOS-0.0.4-arm64.dmg sha512: arm64dmg size: 131754935 -path: CUT3-macOS-0.0.4-arm64.zip +path: Rowl-macOS-0.0.4-arm64.zip sha512: arm64zip releaseDate: '2026-03-07T10:32:14.587Z' `, @@ -27,13 +27,13 @@ releaseDate: '2026-03-07T10:32:14.587Z' const x64 = parseMacUpdateManifest( `version: 0.0.4 files: - - url: CUT3-macOS-0.0.4-x64.zip + - url: Rowl-macOS-0.0.4-x64.zip sha512: x64zip size: 132000112 - - url: CUT3-macOS-0.0.4-x64.dmg + - url: Rowl-macOS-0.0.4-x64.dmg sha512: x64dmg size: 138148807 -path: CUT3-macOS-0.0.4-x64.zip +path: Rowl-macOS-0.0.4-x64.zip sha512: x64zip releaseDate: '2026-03-07T10:36:07.540Z' `, @@ -47,10 +47,10 @@ releaseDate: '2026-03-07T10:36:07.540Z' assert.deepStrictEqual( merged.files.map((file) => file.url), [ - "CUT3-macOS-0.0.4-arm64.zip", - "CUT3-macOS-0.0.4-arm64.dmg", - "CUT3-macOS-0.0.4-x64.zip", - "CUT3-macOS-0.0.4-x64.dmg", + "Rowl-macOS-0.0.4-arm64.zip", + "Rowl-macOS-0.0.4-arm64.dmg", + "Rowl-macOS-0.0.4-x64.zip", + "Rowl-macOS-0.0.4-x64.dmg", ], ); @@ -63,7 +63,7 @@ releaseDate: '2026-03-07T10:36:07.540Z' const arm64 = parseMacUpdateManifest( `version: 0.0.4 files: - - url: CUT3-macOS-0.0.4-arm64.zip + - url: Rowl-macOS-0.0.4-arm64.zip sha512: arm64zip size: 1 releaseDate: '2026-03-07T10:32:14.587Z' @@ -74,7 +74,7 @@ releaseDate: '2026-03-07T10:32:14.587Z' const x64 = parseMacUpdateManifest( `version: 0.0.5 files: - - url: CUT3-macOS-0.0.5-x64.zip + - url: Rowl-macOS-0.0.5-x64.zip sha512: x64zip size: 1 releaseDate: '2026-03-07T10:36:07.540Z' @@ -89,7 +89,7 @@ releaseDate: '2026-03-07T10:36:07.540Z' const manifest = parseMacUpdateManifest( `version: '1.0' files: - - url: CUT3-macOS-1.0-x64.zip + - url: Rowl-macOS-1.0-x64.zip sha512: zipsha size: 1 releaseName: 'true' diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index 21fa74d1b41..8e54aee3b12 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -38,13 +38,13 @@ function writeMacManifestFixtures(targetRoot: string): { arm64Path: string; x64P arm64Path, `version: 9.9.9-smoke.0 files: - - url: CUT3-macOS-9.9.9-smoke.0-arm64.zip + - url: Rowl-macOS-9.9.9-smoke.0-arm64.zip sha512: arm64zip size: 125621344 - - url: CUT3-macOS-9.9.9-smoke.0-arm64.dmg + - url: Rowl-macOS-9.9.9-smoke.0-arm64.dmg sha512: arm64dmg size: 131754935 -path: CUT3-macOS-9.9.9-smoke.0-arm64.zip +path: Rowl-macOS-9.9.9-smoke.0-arm64.zip sha512: arm64zip releaseDate: '2026-03-08T10:32:14.587Z' `, @@ -54,13 +54,13 @@ releaseDate: '2026-03-08T10:32:14.587Z' x64Path, `version: 9.9.9-smoke.0 files: - - url: CUT3-macOS-9.9.9-smoke.0-x64.zip + - url: Rowl-macOS-9.9.9-smoke.0-x64.zip sha512: x64zip size: 132000112 - - url: CUT3-macOS-9.9.9-smoke.0-x64.dmg + - url: Rowl-macOS-9.9.9-smoke.0-x64.dmg sha512: x64dmg size: 138148807 -path: CUT3-macOS-9.9.9-smoke.0-x64.zip +path: Rowl-macOS-9.9.9-smoke.0-x64.zip sha512: x64zip releaseDate: '2026-03-08T10:36:07.540Z' `, @@ -119,19 +119,19 @@ try { const mergedManifest = readFileSync(arm64Path, "utf8"); assertContains( mergedManifest, - "CUT3-macOS-9.9.9-smoke.0-arm64.zip", + "Rowl-macOS-9.9.9-smoke.0-arm64.zip", "Merged manifest is missing the arm64 asset.", ); assertContains( mergedManifest, - "CUT3-macOS-9.9.9-smoke.0-x64.zip", + "Rowl-macOS-9.9.9-smoke.0-x64.zip", "Merged manifest is missing the x64 asset.", ); const checksumAssetPath = resolve( tempRoot, "release-assets", - "CUT3-linux-9.9.9-smoke.0-x86_64.AppImage", + "Rowl-linux-9.9.9-smoke.0-x86_64.AppImage", ); writeFileSync(checksumAssetPath, "linux-release-asset"); execFileSync( @@ -151,7 +151,7 @@ try { const checksumManifest = readFileSync(resolve(tempRoot, "release-assets", "SHA256SUMS"), "utf8"); assertContains( checksumManifest, - "CUT3-linux-9.9.9-smoke.0-x86_64.AppImage", + "Rowl-linux-9.9.9-smoke.0-x86_64.AppImage", "Expected SHA256SUMS to include the Linux release asset.", ); assertContains( diff --git a/turbo.json b/turbo.json index d248e9f44bf..f96f9be370f 100644 --- a/turbo.json +++ b/turbo.json @@ -5,15 +5,15 @@ "VITE_WS_URL", "VITE_DEV_SERVER_URL", "ELECTRON_RENDERER_PORT", - "CUT3_LOG_WS_EVENTS", - "CUT3_MODE", - "CUT3_PORT", - "CUT3_NO_BROWSER", - "CUT3_STATE_DIR", - "CUT3_AUTH_TOKEN", - "CUT3_DESKTOP_WS_URL", - "CUT3_HOST", - "CUT3_AUTO_BOOTSTRAP_PROJECT_FROM_CWD", + "ROWL_LOG_WS_EVENTS", + "ROWL_MODE", + "ROWL_PORT", + "ROWL_NO_BROWSER", + "ROWL_STATE_DIR", + "ROWL_AUTH_TOKEN", + "ROWL_DESKTOP_WS_URL", + "ROWL_HOST", + "ROWL_AUTO_BOOTSTRAP_PROJECT_FROM_CWD", "T3CODE_LOG_WS_EVENTS", "T3CODE_MODE", "T3CODE_PORT",