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
5 changes: 5 additions & 0 deletions .changeset/tall-pillows-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"warpforge": minor
---

Code editing in Warpforge just got a major upgrade. The editor now brings intelligent language support into your workspace: jump from any symbol to its definition with Cmd/Ctrl-click or Cmd+B, see errors and warnings directly in your code, inspect documentation on hover, get completions as you type, find references, rename symbols, and format code. Double-Shift or Cmd/Ctrl+P opens any project file instantly, making large codebases much faster to navigate. Your work stays under your control too: edits are saved only when you explicitly press Save or Cmd/Ctrl+S.
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ it in the same commit as the change). Never hand-edit versions or
`CHANGELOG.md` — the **Version release** workflow owns both. See
`docs/RELEASING.md`.

Changeset text is customer-facing release-note copy. Write it for users, not
maintainers or changelog tooling: lead with outcome and product value, explain
how the feature helps and include a shortcut or action when useful. Use plain,
confident language and describe one coherent user-visible improvement per
changeset; combine related implementation commits when they ship as one
experience. Avoid internal names and implementation details such as RPCs,
packages, daemon processes, file paths, protocol names, or compiler flags.
Mention limitations only when they affect what users can do. Never claim
behavior the product does not provide.

Keep commits small and focused, not huge sweeping changes. Each commit should
briefly describe the essence of what changed (one logical change per commit).

Expand Down
69 changes: 69 additions & 0 deletions crates/warpforge-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ fn default_true() -> bool {
true
}

fn default_search_limit() -> u32 {
200
}

fn default_terminal_cols() -> u16 {
80
}
Expand Down Expand Up @@ -386,6 +390,18 @@ pub enum Method {
},
#[serde(rename = "file.delete")]
FileDelete { task_id: String, path: String },
/// Plain-text search across the task's project working tree (grep). Powers
/// "go to definition" (a symbol under the cursor resolved to its definition
/// lines) and quick symbol lookup, without needing a full LSP server.
#[serde(rename = "file.search")]
FileSearch {
task_id: String,
/// Case-insensitive substring matched against each line.
query: String,
/// Cap on the number of matches returned (cheap safety valve).
#[serde(default = "default_search_limit")]
limit: u32,
},
/// Stage files and commit them in the task's repo. `files=None` stages all
/// changes; `amend` rewrites the previous commit.
#[serde(rename = "git.commit")]
Expand Down Expand Up @@ -598,6 +614,34 @@ pub enum Method {
/// `{ ok, path }`.
#[serde(rename = "bootstrap.writeConfig")]
BootstrapWriteConfig { project: String, yaml: String },

// ── LSP ──
/// Ensure a language server is running for a task's workspace + language.
/// Reuses an existing server for the same (workspace, language). Returns
/// [`LspStartResult`]; `available: false` when no server binary is on PATH.
#[serde(rename = "lsp.start")]
LspStart { task_id: String, language: String },
/// Forward an opaque LSP JSON-RPC message to a running server's stdin.
#[serde(rename = "lsp.send")]
LspSend {
server_id: String,
payload: serde_json::Value,
},
/// Release one reference to a server; the process is killed once the last
/// editor using it closes.
#[serde(rename = "lsp.stop")]
LspStop { server_id: String },
}

/// Reply to [`Method::LspStart`].
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LspStartResult {
pub server_id: String,
pub available: bool,
/// Absolute workspace root the server was rooted at. Clients build
/// `file://` document URIs from it. Empty when unavailable.
pub root_path: String,
}

/// Answers collected by the desktop bootstrap wizard. Mirrors the daemon's
Expand Down Expand Up @@ -769,6 +813,20 @@ pub enum Event {
/// All nodes in the orchestration are done.
#[serde(rename = "orchestration.allComplete")]
OrchestrationAllComplete { graph_id: String, project: String },

// ── LSP ──
/// An opaque LSP JSON-RPC message from a server's stdout.
#[serde(rename = "lsp.message")]
LspMessage {
server_id: String,
payload: serde_json::Value,
},
/// A language server exited (crashed or was stopped).
#[serde(rename = "lsp.exit")]
LspExit {
server_id: String,
code: Option<i32>,
},
}

// ─── State DTOs ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1325,6 +1383,17 @@ pub struct ProjectFile {
pub changed: bool,
}

/// One line-level match from `file.search` — a project path plus 1-based line and
/// column where `query` appears, with the matching source line for context.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SymbolMatch {
pub path: String,
pub line: u32,
pub column: u32,
pub text: String,
}

// ─── Terminal agents (legacy PTY path) ───────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
Expand Down
5 changes: 5 additions & 0 deletions desktop/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/lint": "^6.9.7",
"@codemirror/lsp-client": "^6.2.5",
"@codemirror/merge": "^6.12.2",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.43.4",
Expand Down
40 changes: 40 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { toast } from "sonner";

import AppHeader from "@/components/AppHeader";
import AttentionToast from "@/components/AttentionToast";
import BootstrapWizard from "@/components/BootstrapWizard";
import ErrorBoundary from "@/components/ErrorBoundary";
import { QuickOpen } from "@/components/QuickOpen";
import Sidebar from "@/components/Sidebar";
import { TooltipProvider } from "@/components/ui/tooltip";
import { daemon } from "@/daemon";
Expand All @@ -17,7 +19,9 @@ import { useFontScaling } from "./hooks/useFontScaling";
import { useTheme } from "./hooks/useTheme";
import { usePullShortcut } from "./hooks/usePullShortcut";
import { usePushShortcut } from "./hooks/usePushShortcut";
import { useQuickOpenShortcut } from "./hooks/useQuickOpenShortcut";
import { useTauriClose } from "./hooks/useTauriClose";
import { queryClient, useProjectFileListQuery } from "./query";
import AddProjectDialog from "./views/AddProjectDialog";
import AgentSetupDialog from "./views/AgentSetupDialog";
import MissionControl from "./views/MissionControl";
Expand All @@ -43,6 +47,36 @@ function LiveSidebar(props: Omit<React.ComponentProps<typeof Sidebar>, "state">)
return <Sidebar state={state} {...props} />;
}

/** Hosts the quick-open palette: owns the file-list query and the double-Shift
* trigger. Rendered as a child of the QueryClientProvider so its hook sees the
* client (App's own hooks must not query — they'd render before the provider). */
function QuickOpenHost({
openTaskId,
hasOpenTask,
}: {
openTaskId: string | null;
hasOpenTask: boolean;
}) {
const [open, setOpen] = useState(false);
const filesQuery = useProjectFileListQuery(hasOpenTask ? openTaskId : null);
const openTaskThroughNav = useUi((s) => s.openTaskWithNav);
useQuickOpenShortcut(() => {
if (hasOpenTask) setOpen(true);
});
return (
<QuickOpen
open={open}
files={filesQuery.data ?? []}
loading={filesQuery.isLoading}
error={filesQuery.error?.message ?? null}
onPick={(path) => {
if (openTaskId) openTaskThroughNav(openTaskId, { surface: "files", path });
}}
onClose={() => setOpen(false)}
/>
);
}

const getSnapshot = () => daemon.getState().snapshot;
const getConnection = () => daemon.getState().connection;
const getConnectionError = () => daemon.getState().connectionError;
Expand Down Expand Up @@ -232,6 +266,7 @@ export default function App() {
const persistentWidth = sidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : sidebarWidth;

return (
<QueryClientProvider client={queryClient}>
<TooltipProvider delayDuration={300}>
{/* Prototype shell: full-height sidebar beside a column of topbar + content. */}
<div className="relative flex h-screen bg-background">
Expand Down Expand Up @@ -300,6 +335,10 @@ export default function App() {
</div>

{pushOpen && <PushDialog open onOpenChange={setPushOpen} task={openTask} />}
<QuickOpenHost
openTaskId={openTask ? openTask.id : null}
hasOpenTask={!!openTask && !newTaskOpen}
/>
{addProjectOpen && (
<AddProjectDialog open onOpenChange={setAddProjectOpen} onAdded={handleProjectAdded} />
)}
Expand Down Expand Up @@ -353,5 +392,6 @@ export default function App() {
)}
</div>
</TooltipProvider>
</QueryClientProvider>
);
}
Loading
Loading