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
3 changes: 2 additions & 1 deletion apps/app/src/features/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export function Chat({
<ChatSessionProvider cwd={cwd} sessionRef={sessionRef}>
<div className={cn("flex min-h-0 flex-1 flex-col", className)}>
<ChatTranscript />
<div className="mx-auto w-full max-w-4xl min-w-80 flex-shrink-0 p-2">
<div className="mx-auto w-full max-w-4xl min-w-80 flex-shrink-0 px-2 pt-2 pb-6">
<ChatInputComposer
cwd={cwd}
toolbar={
<>
{/* Same slot the draft surface puts the harness picker in, so
Expand Down
30 changes: 28 additions & 2 deletions apps/app/src/features/chat/components/chat-input-composer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import type { SessionRecoverySnapshot } from "@vibest/contract";
import {
PromptInput,
Expand All @@ -6,7 +8,8 @@ import {
PromptInputTools,
} from "@vibest/ui/ai-elements/prompt-input";
import { Button } from "@vibest/ui/components/button";
import { Card, CardFrame, CardFrameHeader } from "@vibest/ui/components/card";
import { Card, CardFrame, CardFrameFooter, CardFrameHeader } from "@vibest/ui/components/card";
import { GitBranchIcon } from "lucide-react";
import { type ReactNode, useState } from "react";
import { useStore } from "zustand";

Expand Down Expand Up @@ -130,7 +133,19 @@ function QueuedPromptList({
);
}

export function ChatInputComposer({ toolbar }: { toolbar?: ReactNode }) {
export function ChatInputComposer({
cwd,
toolbar,
}: {
/** Session workspace path, from the route. Used to probe the current branch. */
cwd: string | undefined;
toolbar?: ReactNode;
}) {
const { orpcQueryUtils } = useRouteContext({ from: "__root__" });
const branch = useQuery({
...orpcQueryUtils.git.branch.queryOptions({ input: { cwd: cwd ?? "" } }),
enabled: cwd !== undefined,
});
const { acknowledgeRecovery, prompt, steer, turnInProgress, store } = useChatSession();
const status = useStore(store, (state) => state.session.status);
const activeTurnId = useStore(store, (state) => state.session.activeTurnId);
Expand Down Expand Up @@ -219,6 +234,17 @@ export function ChatInputComposer({ toolbar }: { toolbar?: ReactNode }) {
</PromptInputToolbar>
</ChatInputProvider>
</Card>
{branch.data?.current ? (
<CardFrameFooter className="py-2">
<span
className="text-muted-foreground flex items-center gap-1.5 px-3 text-xs"
title="Current git branch"
>
<GitBranchIcon aria-hidden="true" className="size-3.5 shrink-0" />
<span className="truncate">{branch.data.current}</span>
</span>
</CardFrameFooter>
) : null}
</CardFrame>
);
}
1 change: 1 addition & 0 deletions packages/contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"exports": {
".": "./src/index.ts",
"./fs": "./src/fs.ts",
"./git": "./src/git.ts",
"./harness": "./src/harness.ts",
"./pty": "./src/pty.ts",
"./project": "./src/project.ts",
Expand Down
16 changes: 16 additions & 0 deletions packages/contract/src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { oc } from "@orpc/contract";
import { Schema } from "effect";

import { toStandardSchema } from "./domain";

const CwdInput = Schema.Struct({ cwd: Schema.String });

/** Current HEAD name when the workspace is a git work tree; otherwise null. */
export const GitBranchSchema = Schema.Struct({
current: Schema.Union([Schema.String, Schema.Null]),
});
export type GitBranch = typeof GitBranchSchema.Type;

export const gitContract = {
branch: oc.input(toStandardSchema(CwdInput)).output(toStandardSchema(GitBranchSchema)),
};
4 changes: 3 additions & 1 deletion packages/contract/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fsContract } from "./fs";
import { gitContract } from "./git";
import { harnessContract } from "./harness";
import { projectContract } from "./project";
import { ptyContract } from "./pty";
Expand All @@ -13,8 +14,9 @@ export const contract = {
session: sessionContract,
project: projectContract,
fs: fsContract,
git: gitContract,
pty: ptyContract,
};
export type Contract = typeof contract;

export { fsContract, harnessContract, projectContract, ptyContract, sessionContract };
export { fsContract, gitContract, harnessContract, projectContract, ptyContract, sessionContract };
2 changes: 2 additions & 0 deletions packages/server/src/rpc/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { FileSystem } from "effect/FileSystem";

import type { EventBus } from "../events";
import type { FileSystemService } from "../fs";
import type { GitService } from "../git";
import type {
HarnessAgentRegistry,
HarnessAgentSessionService,
Expand All @@ -23,4 +24,5 @@ export type RpcContext = WithEffectContext<
| ProjectService
| PtyService
| FileSystemService
| GitService
>;
21 changes: 21 additions & 0 deletions packages/server/src/rpc/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import "@orpc/experimental-effect/extensions/effect";
import { implement } from "@orpc/server";
import { gitContract } from "@vibest/contract/git";
import { Effect } from "effect";

import { GitService } from "../git";
import type { RpcContext } from "./context";

const orpc = implement(gitContract).$context<RpcContext>();

export const gitRouter = orpc.router({
branch: orpc.branch.effect(function* ({ input }) {
const git = yield* GitService;
return yield* git.branch(input.cwd).pipe(
Effect.map((summary) => ({ current: summary.current ?? null })),
Effect.catchTag("GitError", () => Effect.succeed({ current: null })),
);
}),
});

export type GitRouter = typeof gitRouter;
2 changes: 2 additions & 0 deletions packages/server/src/rpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { os } from "@orpc/server";

import type { RpcContext } from "./context";
import { fsRouter } from "./fs";
import { gitRouter } from "./git";
import { harnessRouter } from "./harness";
import { projectRouter } from "./project";
import { ptyRouter } from "./pty";
Expand All @@ -14,6 +15,7 @@ export const router = orpc.router({
session: sessionRouter,
project: projectRouter,
fs: fsRouter,
git: gitRouter,
pty: ptyRouter,
});
export type Router = typeof router;
2 changes: 2 additions & 0 deletions packages/server/src/rpc/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Context, Effect, type FileSystem, Layer } from "effect";
import { PathsLayer } from "../config/paths";
import { EventBusLayer } from "../events";
import { FileSystemServiceLayer } from "../fs";
import { GitServiceLayer } from "../git";
import {
type HarnessAgentAdapter,
HarnessAgentRegistry,
Expand Down Expand Up @@ -172,6 +173,7 @@ export const AgentRuntimeLayer = Layer.mergeAll(
HarnessListProvided,
HarnessProbeProvided,
FileSystemServiceLayer.pipe(Layer.provide(PlatformLayer)),
GitServiceLayer,
PtyServiceProvided,
PlatformLayer,
// For the HTTP request app: `HttpStaticServer` needs it to turn a file into a
Expand Down
31 changes: 31 additions & 0 deletions packages/server/test/rpc-git.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { simpleGit } from "simple-git";
import { describe, expect, it } from "vitest";

import { makeRpcTestHarness } from "./rpc-harness";

describe("git router", () => {
it("returns the current branch for a work tree and null otherwise", async () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "vibest-home-"));
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "vibest-git-"));
const bare = fs.mkdtempSync(path.join(os.tmpdir(), "vibest-nogit-"));
fs.writeFileSync(path.join(repo, "a.txt"), "hi");
const git = simpleGit(repo);
await git.raw(["init", "-b", "main"]);
await git.addConfig("user.email", "test@example.com");
await git.addConfig("user.name", "Test");
await git.add(".");
await git.commit("init");

const harness = await makeRpcTestHarness(home);
try {
await expect(harness.client.git.branch({ cwd: repo })).resolves.toEqual({ current: "main" });
await expect(harness.client.git.branch({ cwd: bare })).resolves.toEqual({ current: null });
} finally {
await harness.dispose();
}
});
});
2 changes: 2 additions & 0 deletions packages/server/test/rpc-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Layer, ManagedRuntime } from "effect";
import { layerPaths } from "../src/config/paths";
import { EventBusLayer } from "../src/events";
import { FileSystemServiceLayer } from "../src/fs";
import { GitServiceLayer } from "../src/git";
import {
HarnessAgentRegistry,
HarnessAgentSessionManagerLayer,
Expand Down Expand Up @@ -74,6 +75,7 @@ export async function makeRpcTestHarness(
listLayer,
probeLayer,
FileSystemServiceLayer.pipe(Layer.provide(NodePlatformLayer)),
GitServiceLayer,
ptyLayer,
NodePlatformLayer,
),
Expand Down
2 changes: 2 additions & 0 deletions packages/server/test/rpc-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { describe, expect, it } from "vitest";
import { layerPaths } from "../src/config/paths";
import { EventBusLayer } from "../src/events";
import { FileSystemServiceLayer } from "../src/fs";
import { GitServiceLayer } from "../src/git";
import {
HarnessAgentRegistry,
HarnessAgentSessionManagerLayer,
Expand Down Expand Up @@ -120,6 +121,7 @@ async function setup() {
HarnessListLayer.pipe(Layer.provide(registryLayer), Layer.provide(NodeServices.layer)),
HarnessProbeLayer.pipe(Layer.provide(registryLayer)),
FileSystemServiceLayer.pipe(Layer.provide(NodeServices.layer)),
GitServiceLayer,
ptyServiceLayer,
NodeServices.layer,
Observability.discard,
Expand Down
Loading