Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 2 additions & 1 deletion DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<UpdatePrompt />` 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 `<PromptInputV2ModelControl>` plus the fork-only `<PromptInputV2PermissionControl>`, 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 `<PromptInputV2ModelControl>` 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 `<PromptInputV2ModelControl>` plus the fork-only `<PromptInputV2PermissionControl>`, 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 `<PromptInputV2ModelControl>` 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

Expand Down
27 changes: 23 additions & 4 deletions packages/app/src/components/prompt-input-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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. */}
<PromptInputV2PermissionControl onClose={props.controller.restoreFocus} />
<PromptInputV2ModelControl
loading={props.controller.model.loading}
paid={props.controller.model.paid}
Expand All @@ -84,9 +88,7 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
})
}}
/>
{/* Fork: rides the model slot rather than a slot of its own so it
lands immediately right of the model select. */}
<PromptInputV2PermissionControl onClose={props.controller.restoreFocus} />
<PromptInputV2BranchIndicator />
</>
}
/>
Expand Down Expand Up @@ -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 (
<Show when={params.id}>
{/* Match the ghost-muted control buttons around it. */}
<div class="flex min-w-0 items-center text-v2-text-text-muted">
<PromptGitStatus branch={sync().data.vcs?.branch} separator={false} />
</div>
</Show>
)
}

// 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 }) {
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/components/prompt-workspace-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -109,7 +109,9 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) {
<Show when={label()}>
{(value) => (
<>
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
<Show when={props.separator ?? true}>
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
</Show>
<TooltipV2
placement="top"
value={value()}
Expand Down
Loading