diff --git a/CHANGELOG.md b/CHANGELOG.md index 4534b25b8..91c654e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ file is the human-readable summary of what changed and why. ## Unreleased +### Sessions + +- **Git branch in the session composer** — open sessions now show the current + git branch of their directory in the composer's control row, right of the + model picker, on both Code and Work (a sandbox session shows the guest + checkout's branch) — the same chip the new-session screen shows beside the + project selector. The Auto/Manual permission dropdown also moved next to + the agent (Plan/Build) dropdown, so the mode controls sit together ahead + of the model select. + ([#339](https://github.com/quickbeard/codev-code/pull/339)) + ### Gateway - **Per-model context limits** — configuring the gateway (the TUI `/configure` diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 840fbb08c..f9adc5535 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -333,8 +333,9 @@ from the squash message first, or use a plain merge. | `packages/app/src/pages/layout-new.tsx` (update prompt) | One import and one element: mounts the fork-only `src/components/dialog-update.tsx`, whose `` renders nothing and opens the update dialog the first time the desktop updater reports `available` or `ready`. | The main process has always checked for updates at launch (`updater.start()`), but on the new layout the only thing that ever said so was a small Update button in the titlebar, and only once the download had finished. Users went weeks without noticing a waiting update. The legacy layout's `UpdateAvailableToast` in `pages/layout.tsx` is unreachable — `newLayoutDesigns` is always true. | Keep the import and the element; take upstream's other changes. If upstream reworks this file's layout, the element can sit anywhere inside the shell: it renders nothing. | | Home-directory crawl guard: `packages/core/src/filesystem/search.ts`, `packages/app/src/components/directory-picker-domain.ts`, `packages/desktop/src/main/index.ts`, `packages/core/test/filesystem/search.test.ts`, `packages/app/src/components/directory-picker-domain.test.ts` | search.ts: new exported `unindexableDirectory(directory)` (true for `Global.Path.home` — honoring `CODEV_TEST_HOME` — and filesystem roots) and a new `shallowLayer`; the layer chooser becomes `Layer.unwrap(Effect.gen(...))` reading `Location.Service` and picks `shallowLayer` for unindexable directories, fff/ripgrep otherwise. `shallowLayer.find` answers from one `readDirectoryEntries` of the location itself (direct children only, symlinks/specials dropped, same fuzzysort/empty-query/limit semantics as the index layers); its glob/grep are the on-demand ripgrep implementations, extracted from `ripgrepLayer` into a shared `ripgrepGlobGrep` effect (ripgrepLayer now spreads it — no behavior change there). directory-picker-domain.ts: `createDirectorySearch`'s bare-word branch (no `/`, `~`, or drive root in the input) matches direct children via the existing `file.list`-based `match()` when the base is the picker's home or a filesystem root, instead of `find.files({directory: base})`. desktop index.ts: the startup `process.chdir(homedir())` becomes `process.chdir(join(app.getPath("userData"), "cwd"))` (mkdir first, after `app.setPath("userData", ...)` so it lands under the channel's own folder; still inside try/catch, `homedir` import dropped). | Materializing the search service walks everything under its location — fff at creation, ripgrep as an eagerly forked scan. Rooted at `~` that walk opens files inside Downloads, Desktop, Documents and the photo/music library bundles, so macOS raised a permission prompt for each (confirmed in the TCC database: Desktop, Media Library, Downloads granted; Photos denied — the last two one second apart, one walk). The desktop app reached a home-rooted location two ways: the picker's bare-word search runs `find.files` against its `~` base, and any server request naming no directory falls back to `process.cwd()` (workspace-routing), which the sidecar inherited as `~` from the main process's chdir. Reproduced end to end against a decoy home: one directoryless `find/file` request read sentinel files inside `Downloads/` and `Photos Library.photoslibrary/`; with the guard the same request returns home's direct children and no sentinel is touched. | Keep all three pieces and take upstream's surrounding changes. If upstream reworks the search layers, preserve the invariant: no eager index (fff create or ripgrep scan) is ever built for the home directory or a filesystem root — `find` there must answer from a single readdir of the location itself. If upstream moves the desktop chdir, keep it pointed at an app-owned empty folder, never `homedir()` (and never leave it in `/`, which breaks ripgrep). The picker guard is belt-and-braces for older/remote servers; do not "simplify" it away because the server also guards. | -| Prompt permission mode: `packages/app/src/components/prompt-input-v2.tsx`, `packages/app/src/context/permission.tsx`, `packages/app/src/pages/session/use-session-commands.tsx`, `packages/app/src/components/settings-general.tsx`, `packages/app/src/components/settings-v2/general.tsx`, `packages/app/src/i18n/{en,es,vi}.ts` | prompt-input-v2.tsx: the `modelControl` slot now carries a fragment — upstream's `` plus the fork-only ``, an Auto/Manual select built on session-ui's exported `PromptInputV2Select` (riding the model slot keeps it immediately right of the model select without a new slot in session-ui). It reads and writes the same client-side auto-accept state the `permissions.autoaccept` command toggles: a session takes an explicit override, a draft steers the directory default (`src/components/prompt-input/permission-mode.ts`, fork-only, holds that decision). A directory whose server config says `permission: "allow"` renders a non-interactive Auto with a tooltip, since such a server never asks. permission.tsx: `enableConfiguredDirectory` gains an `autoByDefault` parameter, passed as `sideForServerKey(activeServer()) === "work"`, so Work directories seed to auto-accept, and exposes `side()` so the consent gate agrees with it. All three paths that turn auto-accept ON — the select, the `permissions.autoaccept` command in use-session-commands.tsx, and the Settings switch in both general.tsx files — now await `useAutoAcceptConsent()` (fork-only `src/components/dialog-auto-accept.tsx`): on Code, once per app run, a confirm dialog focused on Cancel plus a warning toast, matching the TUI's `enableAutoApprove`; Work never asks, since auto is the default there that nobody picked. Turning it off never asks. i18n: the `prompt.permission.*` and `dialog.autoAccept.*` families. | Users asked to pick Auto/Manual per session, with Code defaulting to manual and Work to auto. The Work default used to come from the sandbox guest config (`permission: "allow"`, dropped in `packages/desktop/src/main/sandbox/egress.ts` — fork-only, untracked here): a server told to allow everything never emits `permission.asked`, so Manual had nothing to gate. Moving the default to the host makes both modes real on both sides while leaving the walk-away behaviour unchanged. | Keep the fork's fragment and re-apply upstream's changes to the `` inside it. The three enable paths each gained one `await consent(...)` line — keep them together, or the warning becomes theatre that one door skips. If upstream adds its own permission or mode control to the prompt toolbar, drop ours and re-point the Work default at whatever state theirs uses. `enableConfiguredDirectory`'s extra parameter is additive — if upstream rewrites that function, keep the `autoByDefault` branch or the Work side loses its auto default. | +| Prompt permission mode: `packages/app/src/components/prompt-input-v2.tsx`, `packages/app/src/context/permission.tsx`, `packages/app/src/pages/session/use-session-commands.tsx`, `packages/app/src/components/settings-general.tsx`, `packages/app/src/components/settings-v2/general.tsx`, `packages/app/src/i18n/{en,es,vi}.ts` | prompt-input-v2.tsx: the `modelControl` slot now carries a fragment — upstream's `` plus the fork-only ``, an Auto/Manual select built on session-ui's exported `PromptInputV2Select` (riding the model slot avoids a new slot in session-ui; it leads the fragment, so it sits between the agent select and the model select). It reads and writes the same client-side auto-accept state the `permissions.autoaccept` command toggles: a session takes an explicit override, a draft steers the directory default (`src/components/prompt-input/permission-mode.ts`, fork-only, holds that decision). A directory whose server config says `permission: "allow"` renders a non-interactive Auto with a tooltip, since such a server never asks. permission.tsx: `enableConfiguredDirectory` gains an `autoByDefault` parameter, passed as `sideForServerKey(activeServer()) === "work"`, so Work directories seed to auto-accept, and exposes `side()` so the consent gate agrees with it. All three paths that turn auto-accept ON — the select, the `permissions.autoaccept` command in use-session-commands.tsx, and the Settings switch in both general.tsx files — now await `useAutoAcceptConsent()` (fork-only `src/components/dialog-auto-accept.tsx`): on Code, once per app run, a confirm dialog focused on Cancel plus a warning toast, matching the TUI's `enableAutoApprove`; Work never asks, since auto is the default there that nobody picked. Turning it off never asks. i18n: the `prompt.permission.*` and `dialog.autoAccept.*` families. | Users asked to pick Auto/Manual per session, with Code defaulting to manual and Work to auto. The Work default used to come from the sandbox guest config (`permission: "allow"`, dropped in `packages/desktop/src/main/sandbox/egress.ts` — fork-only, untracked here): a server told to allow everything never emits `permission.asked`, so Manual had nothing to gate. Moving the default to the host makes both modes real on both sides while leaving the walk-away behaviour unchanged. | Keep the fork's fragment and re-apply upstream's changes to the `` inside it. The three enable paths each gained one `await consent(...)` line — keep them together, or the warning becomes theatre that one door skips. If upstream adds its own permission or mode control to the prompt toolbar, drop ours and re-point the Work default at whatever state theirs uses. `enableConfiguredDirectory`'s extra parameter is additive — if upstream rewrites that function, keep the `autoByDefault` branch or the Work side loses its auto default. | | Message order across the ID wrap: `packages/opencode/src/session/{message-v2,prompt}.ts` (+ tests) | Server side, `MessageV2` exports `chronological(a, b)` ((time.created, id) — upstream's private `isAfter` in `latest()` is the same order and is taken as-is), and the prompt loop's already-answered exit compares `MessageV2.chronological(lastUser, lastAssistant) < 0` instead of upstream's `lastAssistant.parentID === lastUser.id`. Client side the divergence is GONE: upstream v1.18.19 converged on (time.created, id) ordering (`utils/session-message.ts` `messageKey`/`compareMessages`, linear `findIndex` for ID-only lookups), so the fork's `utils/message-order.ts` was deleted and its call sites now use upstream's helpers. | Upstream's ID encoder keeps only 48 bits of `timestamp*4096`, so the time prefix wraps every 2^36 ms (last on 2026-08-14). Assistants persisted by the fork before `parentID` existed would fail upstream's equality check and read as unanswered — the prompt loop would re-run a turn on old sessions — so the chronological comparison stays. | Keep the `chronological` export and the prompt-loop comparison; take upstream's surrounding changes. If upstream's `parentID` is ever backfilled into stored fork data, their equality check can replace this. | +| Composer git branch indicator: `packages/app/src/components/prompt-input-v2.tsx`, `packages/app/src/components/prompt-workspace-selector.tsx` | The composer's `modelControl` fragment (see the prompt-permission-mode row) gains a third element after the model select: the fork-only `PromptInputV2BranchIndicator`, which feeds `sync().data.vcs?.branch` into the new-session `PromptGitStatus` chip, wrapped in `text-v2-text-text-muted` so it matches the ghost-muted control buttons beside it, and gated on a session id in the route so the new-session views (which already show the branch beside the project selector) don't render it twice. `PromptGitStatus` gains a `separator?: boolean` prop (default `true`) so the footer usage drops the leading "/" divider that the new-session row needs. | Existing sessions never showed which branch the session's directory is on. The new-session chip already renders it for both Code and Work servers — per-server sync supplies the guest branch on Work — so it is reused rather than duplicated. | Keep the `separator` prop and the indicator; take upstream's surrounding prompt-input and workspace-selector changes. If upstream adds its own branch/status chip to the composer footer, prefer theirs and drop ours. | ## Known rebrand gaps diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx index ddf8e90a2..3e6751897 100644 --- a/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app/src/components/prompt-input-v2.tsx @@ -14,6 +14,7 @@ import type { PromptInputProps } from "@/components/prompt-input/contracts" import { normalizePromptHistoryEntry, promptLength, type PromptHistoryComment } from "@/components/prompt-input/history" import { createPersistedPromptInputHistory } from "@/components/prompt-input/history-store" import { permissionModeAction, type PromptPermissionMode } from "@/components/prompt-input/permission-mode" +import { PromptGitStatus } from "@/components/prompt-workspace-selector" import { promptDesignPlaceholder, promptPlaceholder } from "@/components/prompt-input/placeholder" import { createPromptSubmit } from "@/components/prompt-input/submit" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" @@ -66,6 +67,9 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { attachShortcut={command.keybind("file.attach")} modelControl={ <> + {/* Fork: rides the model slot rather than a slot of its own; leading + the fragment lands it between the agent select and the model select. */} + - {/* Fork: rides the model slot rather than a slot of its own so it - lands immediately right of the model select. */} - + } /> @@ -566,7 +568,24 @@ function PromptInputV2ModelControl(props: { ) } -// Fork: Auto/Manual permission mode for the session, beside the model select. +// Fork: current git branch of the session's directory, reusing the new-session +// status chip beside the model select. Existing sessions only — the new-session +// views already show the branch beside the project selector under the composer. +function PromptInputV2BranchIndicator() { + const sync = useSync() + const params = useParams<{ id?: string }>() + + return ( + + {/* Match the ghost-muted control buttons around it. */} +
+ +
+
+ ) +} + +// Fork: Auto/Manual permission mode for the session, beside the agent select. // "Auto" is the same client-side auto-accept the `permissions.autoaccept` // command toggles, so the select and the command always agree. function PromptInputV2PermissionControl(props: { onClose: () => void }) { diff --git a/packages/app/src/components/prompt-workspace-selector.tsx b/packages/app/src/components/prompt-workspace-selector.tsx index a7a353743..06e7c8af8 100644 --- a/packages/app/src/components/prompt-workspace-selector.tsx +++ b/packages/app/src/components/prompt-workspace-selector.tsx @@ -98,7 +98,7 @@ export function PromptWorkspaceSelector(props: { ) } -export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) { +export function PromptGitStatus(props: { branch?: string; noGit?: boolean; separator?: boolean }) { const language = useLanguage() const label = () => { if (props.noGit) return language.t("session.new.git.none") @@ -109,7 +109,9 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) { {(value) => ( <> - + + +