Skip to content
Open
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
10 changes: 5 additions & 5 deletions extensions/workspace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Search, pin, and open projects from every folder you work in.

Workspace is a [Vicinae](https://vicinae.com) extension. Add workspace folders and it lists every project inside, with git status, per-workspace opening app, and a JSON backup of your setup.
Workspace is a [Vicinae](https://vicinae.com) extension. Add workspace folders and it lists every project inside, with git status, a custom app per workspace, and a JSON backup of your setup.

## Screenshots

Expand All @@ -29,13 +29,13 @@ Workspace is a [Vicinae](https://vicinae.com) extension. Add workspace folders a
2. Set a default app (and a terminal, if you want) in **Workspace Settings**.
3. Open **Workspace**, search, and hit enter.

Pin the `Workspace` command in Vicinae if you want the fastest loop.
Pin the `Workspace` command in Vicinae to reach it faster.

## Commands

- **Workspace** – Search and open projects
- **Manage Workspaces** – Add, remove, and reorder workspace folders
- **Workspace Settings** – Apps, git status, backup, and reset
- **Workspace**: Search and open projects
- **Manage Workspaces**: Add, remove, and reorder workspace folders
- **Workspace Settings**: Apps, git status, backup, and reset

## Installation

Expand Down
4 changes: 2 additions & 2 deletions extensions/workspace/package-lock.json

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

4 changes: 2 additions & 2 deletions extensions/workspace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"$schema": "https://raw.githubusercontent.com/vicinaehq/vicinae/refs/heads/main/extra/schemas/extension.json",
"name": "workspace",
"title": "Workspace",
"description": "Manage and access your workspace projects. Boost your focus with quick project opening, git status tracking, and customizable application overrides.",
"description": "Open your workspace projects fast, with git status and a custom app per workspace.",
"icon": "extension-icon.png",
"author": "tiagem",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT",
"platforms": [
"Linux",
Expand Down
46 changes: 35 additions & 11 deletions extensions/workspace/src/components/AddWorkspaceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Action, ActionPanel, Form, popToRoot, showToast, Toast, useNavigation } from "@vicinae/api";
import { Action, ActionPanel, Form, Icon, popToRoot, showToast, Toast, useNavigation } from "@vicinae/api";
import path from "path";
import { useState } from "react";
import { useMemo, useState } from "react";

import { useWorkspace } from "@/hooks/useWorkspace";
import { isExistingDirectory, pathFromFormValue } from "@/utils/paths";
import { suggestedWorkspacePaths } from "@/utils/suggestions";

interface AddWorkspaceFormProps {
onDone?: () => Promise<void> | void;
Expand All @@ -13,14 +14,9 @@ export default function AddWorkspaceForm({ onDone }: AddWorkspaceFormProps) {
const { pop } = useNavigation();
const { updateWorkspaces, workspaces } = useWorkspace();
const [error, setError] = useState<string | undefined>();
const suggestions = useMemo(() => suggestedWorkspacePaths(workspaces), [workspaces]);

async function handleSubmit(values: Form.Values) {
const workspacePath = pathFromFormValue(values.workspace);
if (!workspacePath) {
setError("Required");
return;
}

async function addWorkspace(workspacePath: string) {
if (!isExistingDirectory(workspacePath)) {
setError("Folder not found");
await showToast({
Expand Down Expand Up @@ -58,21 +54,49 @@ export default function AddWorkspaceForm({ onDone }: AddWorkspaceFormProps) {
}
}

async function handleSubmit(values: Form.Values) {
const workspacePath = pathFromFormValue(values.workspace);
if (!workspacePath) {
setError("Required");
return;
}

await addWorkspace(workspacePath);
}

return (
<Form
actions={
<ActionPanel>
<ActionPanel.Section title="Workspace">
<Action.SubmitForm onSubmit={handleSubmit} title="Add Workspace" />
<Action.SubmitForm icon={Icon.Folder} onSubmit={handleSubmit} title="Add Workspace" />
</ActionPanel.Section>
{suggestions.length > 0 ? (
<ActionPanel.Section title="Suggestions">
{suggestions.map((suggestion) => (
<Action
key={suggestion}
icon={Icon.Folder}
onAction={() => addWorkspace(suggestion)}
title={`Add ${path.basename(suggestion)}`}
/>
))}
</ActionPanel.Section>
) : null}
</ActionPanel>
}
navigationTitle="Add Workspace"
>
<Form.Description
text="Pick a parent folder that contains your projects. Each top-level folder inside it becomes a project."
text="Pick a parent folder that contains your projects. Scan depth and ignore patterns in Settings control what is listed."
title="How It Works"
/>
{suggestions.length > 0 ? (
<Form.Description
text={`Found on this machine: ${suggestions.map((suggestion) => path.basename(suggestion)).join(", ")}. Use the action panel to add one quickly.`}
title="Suggestions"
/>
) : null}
<Form.FilePicker
canChooseDirectories
canChooseFiles={false}
Expand Down
11 changes: 7 additions & 4 deletions extensions/workspace/src/components/Git/CheckoutBranch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ export default function CheckoutBranch({ onBranchChanged, project }: CheckoutBra
}

const toast = await showToast({ style: Toast.Style.Animated, title: `Checking out ${branch}...` });
const success = await checkoutGitBranch(project.fullPath, branch);
const result = await checkoutGitBranch(project.fullPath, branch);

if (success) {
if (result.ok) {
toast.style = Toast.Style.Success;
toast.title = `Checked out ${branch}`;
if (onBranchChanged) onBranchChanged();
pop();
} else {
toast.style = Toast.Style.Failure;
toast.title = "Checkout failed";
toast.message = "Check for uncommitted changes.";
toast.message = result.message;
}
};

Expand All @@ -63,6 +63,9 @@ export default function CheckoutBranch({ onBranchChanged, project }: CheckoutBra
title="Couldn't Load Branches"
/>
)}
{!error && !isLoading && (branches?.length ?? 0) === 0 && (
<List.EmptyView description="This repository has no local branches." title="No Local Branches" />
)}
{(branches ?? []).map((branch) => {
const isCurrent = branch === currentBranch;
return (
Expand All @@ -73,7 +76,7 @@ export default function CheckoutBranch({ onBranchChanged, project }: CheckoutBra
{!isCurrent && (
<Action icon={Icon.ArrowRight} onAction={() => handleCheckout(branch)} title="Checkout Branch" />
)}
<Action.CopyToClipboard content={branch} title="Copy Branch Name" />
<Action.CopyToClipboard content={branch} icon={Icon.CopyClipboard} title="Copy Branch Name" />
</ActionPanel>
}
icon={Icon.Shuffle}
Expand Down
4 changes: 3 additions & 1 deletion extensions/workspace/src/components/Git/CommitLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ export default function CommitLog({ project }: CommitLogProps) {
<ActionPanel>
{data?.remoteUrl && (
<Action.OpenInBrowser
icon={Icon.Globe01}
title="Open Commit in Browser"
url={commitBrowserUrl(data.remoteUrl, commit.hash)}
/>
)}
<Action.CopyToClipboard content={commit.hash} title="Copy Commit Hash" />
<Action.CopyToClipboard content={commit.hash} icon={Icon.CopyClipboard} title="Copy Commit Hash" />
<Action.CopyToClipboard
content={commit.message}
icon={Icon.CopyClipboard}
shortcut={{ key: "c", modifiers: ["cmd", "shift"] }}
title="Copy Commit Message"
/>
Expand Down
4 changes: 2 additions & 2 deletions extensions/workspace/src/components/ImportSettingsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, ActionPanel, Form, showToast, Toast, useNavigation } from "@vicinae/api";
import { Action, ActionPanel, Form, Icon, showToast, Toast, useNavigation } from "@vicinae/api";
import { useState } from "react";

import { isExistingFile, pathFromFormValue } from "@/utils/paths";
Expand Down Expand Up @@ -39,7 +39,7 @@ export default function ImportSettingsForm({ onImport }: ImportSettingsFormProps
actions={
<ActionPanel>
<ActionPanel.Section title="Import">
<Action.SubmitForm onSubmit={handleSubmit} title="Import Settings" />
<Action.SubmitForm icon={Icon.Upload} onSubmit={handleSubmit} title="Import Settings" />
</ActionPanel.Section>
</ActionPanel>
}
Expand Down
40 changes: 39 additions & 1 deletion extensions/workspace/src/components/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Action, ActionPanel, Color, Icon, List, type Application } from "@vicinae/api";
import { Action, ActionPanel, Color, Icon, List, showToast, Toast, type Application } from "@vicinae/api";
import path from "path";
import { useMemo } from "react";

import AddWorkspaceForm from "@/components/AddWorkspaceForm";
import ImportSettingsForm from "@/components/ImportSettingsForm";
import SelectEditor from "@/components/SelectEditor";
import Settings from "@/components/Settings";
import { useWorkspace } from "@/hooks/useWorkspace";
import { App } from "@/types";
import { suggestedWorkspacePaths } from "@/utils/suggestions";

interface OnboardingProps {
defaultApp: App | null;
Expand All @@ -23,10 +27,26 @@ export default function Onboarding({
onSelectDefaultApp,
workspaces,
}: OnboardingProps) {
const { updateWorkspaces } = useWorkspace();
const hasWorkspaces = workspaces.length > 0;
const hasApp = !!defaultApp;
const isReady = hasWorkspaces;
const nextStep = !hasWorkspaces ? "Add your first workspace" : "Finish onboarding";
const suggestions = useMemo(() => suggestedWorkspacePaths(workspaces), [workspaces]);

const addSuggested = async (workspacePath: string) => {
if (workspaces.includes(workspacePath)) {
return;
}

await updateWorkspaces([...workspaces, workspacePath]);
await loadData();
await showToast({
message: path.basename(workspacePath),
style: Toast.Style.Success,
title: "Workspace Added",
});
};

return (
<List navigationTitle="Workspace Onboarding">
Expand Down Expand Up @@ -99,6 +119,24 @@ export default function Onboarding({
/>
)}
</List.Section>
{suggestions.length > 0 ? (
<List.Section title="Suggested Folders">
{suggestions.map((suggestion) => (
<List.Item
key={suggestion}
actions={
<ActionPanel>
<Action icon={Icon.Plus} onAction={() => addSuggested(suggestion)} title="Add Workspace" />
<AddWorkspaceAction loadData={loadData} />
</ActionPanel>
}
icon={Icon.Folder}
subtitle={suggestion}
title={path.basename(suggestion)}
/>
))}
</List.Section>
) : null}
<List.Section title="Import Existing Setup">
<List.Item
actions={
Expand Down
Loading
Loading