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
190 changes: 117 additions & 73 deletions packages/app/src/app/components/workspace-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { For, Show, createEffect, createMemo } from "solid-js";

import { Check, Globe, Loader2, Plus, Search, Trash2, Upload } from "lucide-solid";
import { Check, Globe, Loader2, Plus, Search, Trash2, Upload, X } from "lucide-solid";
import { t, currentLocale } from "../../i18n";

import type { WorkspaceInfo } from "../lib/tauri";
Expand Down Expand Up @@ -35,6 +35,17 @@ export default function WorkspacePicker(props: {
});

const totalCount = createMemo(() => props.workspaces.length);
const filteredCount = createMemo(() => filtered().length);
const query = createMemo(() => props.search.trim());
const hasSearch = createMemo(() => query().length > 0);
const countLabel = createMemo(() => (totalCount() ? `${filteredCount()}/${totalCount()}` : `${filteredCount()}`));
const emptyTitle = createMemo(() => {
if (!totalCount()) return translate("dashboard.workspaces_empty");
if (hasSearch()) {
return translate("dashboard.workspaces_no_results").replace("{query}", query());
}
return translate("dashboard.workspaces_empty");
});
let searchInputRef: HTMLInputElement | undefined;

createEffect(() => {
Expand Down Expand Up @@ -62,90 +73,123 @@ export default function WorkspacePicker(props: {
placeholder={translate("dashboard.find_workspace")}
value={props.search}
onInput={(e) => props.onSearch(e.currentTarget.value)}
class="w-full bg-gray-1 border border-gray-6 rounded-lg py-1.5 pl-9 pr-3 text-sm text-gray-12 focus:outline-none focus:border-gray-7"
class="w-full bg-gray-1 border border-gray-6 rounded-lg py-1.5 pl-9 pr-9 text-sm text-gray-12 focus:outline-none focus:border-gray-7"
/>
<Show when={hasSearch()}>
<button
type="button"
class="absolute right-2 top-2.5 text-gray-9 hover:text-gray-12"
aria-label={translate("dashboard.workspaces_clear_search")}
onClick={() => props.onSearch("")}
>
<X size={12} />
</button>
</Show>
</div>
</div>

<div class="max-h-64 overflow-y-auto p-1">
<div class="px-3 py-2 text-[10px] font-semibold text-gray-10 uppercase tracking-wider">
{translate("dashboard.workspaces")} ({totalCount()})
{translate("dashboard.workspaces")} ({countLabel()})
</div>

<For each={filtered()}>
{(ws) => (
<div
class={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${
props.activeWorkspaceId === ws.id
? "bg-gray-4 text-gray-12"
: "text-gray-11 hover:text-gray-12 hover:bg-gray-4/50"
}`}
>
<button
onClick={() => {
const result = props.onSelect(ws.id);
if (result instanceof Promise) {
result.then((ok) => {
if (ok !== false) props.onClose();
});
return;
}
if (result !== false) props.onClose();
}}
class="flex-1 text-left min-w-0"
>
<div class="flex items-center gap-2">
<div class="font-medium truncate">{ws.name}</div>
<Show when={ws.workspaceType === "remote"}>
<span class="inline-flex items-center gap-1 text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-3 text-gray-11">
<Globe size={10} />
{translate("dashboard.remote")}
</span>
<span class="inline-flex items-center text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-2 text-gray-10">
{ws.remoteType === "openwork"
? translate("dashboard.remote_connection_openwork")
: translate("dashboard.remote_connection_direct")}
</span>
</Show>
</div>
<div class="text-[10px] text-gray-7 font-mono truncate max-w-[200px]">
{ws.workspaceType === "remote"
? ws.remoteType === "openwork"
? ws.openworkHostUrl ?? ws.baseUrl ?? ws.path
: ws.baseUrl ?? ws.path
: ws.path}
</div>
<Show
when={
ws.workspaceType === "remote" &&
(ws.directory || ws.openworkWorkspaceName)
}
<Show
when={filteredCount()}
fallback={
<div class="px-3 py-8 text-xs text-gray-9 text-center space-y-2">
<div>{emptyTitle()}</div>
<Show when={hasSearch()}>
<button
type="button"
class="text-xs text-gray-10 hover:text-gray-12 underline"
onClick={() => props.onSearch("")}
>
<div class="text-[10px] text-gray-8 truncate max-w-[200px]">
{ws.openworkWorkspaceName ?? ws.directory}
</div>
</Show>
</button>
<Show when={props.activeWorkspaceId === ws.id}>
<Check size={14} class="text-indigo-11" />
{translate("dashboard.workspaces_clear_search")}
</button>
</Show>
<Show when={props.connectingWorkspaceId === ws.id}>
<Loader2 size={14} class="text-gray-10 animate-spin" />
<Show when={!totalCount() && !hasSearch()}>
<div class="text-[10px] text-gray-8">
{translate("dashboard.workspaces_empty_hint")}
</div>
</Show>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
props.onForget(ws.id);
}}
class="p-1 rounded-md text-gray-9 hover:text-gray-12 hover:bg-gray-3 transition-colors"
title={translate("dashboard.forget_workspace")}
>
<Trash2 size={14} />
</button>
</div>
)}
</For>
}
>
<For each={filtered()}>
{(ws) => (
<div
class={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${
props.activeWorkspaceId === ws.id
? "bg-gray-4 text-gray-12"
: "text-gray-11 hover:text-gray-12 hover:bg-gray-4/50"
}`}
>
<button
onClick={() => {
const result = props.onSelect(ws.id);
if (result instanceof Promise) {
result.then((ok) => {
if (ok !== false) props.onClose();
});
return;
}
if (result !== false) props.onClose();
}}
class="flex-1 text-left min-w-0"
>
<div class="flex items-center gap-2">
<div class="font-medium truncate">{ws.name}</div>
<Show when={ws.workspaceType === "remote"}>
<span class="inline-flex items-center gap-1 text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-3 text-gray-11">
<Globe size={10} />
{translate("dashboard.remote")}
</span>
<span class="inline-flex items-center text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-2 text-gray-10">
{ws.remoteType === "openwork"
? translate("dashboard.remote_connection_openwork")
: translate("dashboard.remote_connection_direct")}
</span>
</Show>
</div>
<div class="text-[10px] text-gray-7 font-mono truncate max-w-[200px]">
{ws.workspaceType === "remote"
? ws.remoteType === "openwork"
? ws.openworkHostUrl ?? ws.baseUrl ?? ws.path
: ws.baseUrl ?? ws.path
: ws.path}
</div>
<Show
when={
ws.workspaceType === "remote" &&
(ws.directory || ws.openworkWorkspaceName)
}
>
<div class="text-[10px] text-gray-8 truncate max-w-[200px]">
{ws.openworkWorkspaceName ?? ws.directory}
</div>
</Show>
</button>
<Show when={props.activeWorkspaceId === ws.id}>
<Check size={14} class="text-indigo-11" />
</Show>
<Show when={props.connectingWorkspaceId === ws.id}>
<Loader2 size={14} class="text-gray-10 animate-spin" />
</Show>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
props.onForget(ws.id);
}}
class="p-1 rounded-md text-gray-9 hover:text-gray-12 hover:bg-gray-3 transition-colors"
title={translate("dashboard.forget_workspace")}
>
<Trash2 size={14} />
</button>
</div>
)}
</For>
</Show>
</div>

<div class="p-2 border-t border-gray-6 bg-gray-2">
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/app/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ export default function DashboardView(props: DashboardViewProps) {
refreshJobs={props.refreshScheduledJobs}
deleteJob={props.deleteScheduledJob}
isWindows={props.isWindows}
openPlugins={() => props.setTab("plugins")}
/>
</Match>
<Match when={props.tab === "commands"}>
Expand Down
14 changes: 11 additions & 3 deletions packages/app/src/app/pages/scheduled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Button from "../components/button";
import {
Calendar,
Clock,
Cpu,
FolderOpen,
RefreshCw,
Terminal,
Expand All @@ -23,6 +24,7 @@ export type ScheduledTasksViewProps = {
refreshJobs: (options?: { force?: boolean }) => void;
deleteJob: (name: string) => Promise<void> | void;
isWindows: boolean;
openPlugins: () => void;
};

const toRelative = (value?: string | null) => {
Expand Down Expand Up @@ -194,9 +196,15 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
<Show
when={props.jobs.length}
fallback={
<div class="px-6 py-10 text-sm text-gray-10">
No scheduled tasks yet. Add the opencode-scheduler plugin and create a job to
see it here.
<div class="px-6 py-10 text-sm text-gray-10 space-y-4">
<div>
No scheduled tasks yet. Add the opencode-scheduler plugin and create a job to
see it here.
</div>
<Button variant="secondary" onClick={props.openPlugins}>
<Cpu size={16} />
Open plugins
</Button>
</div>
}
>
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default {
"dashboard.runs": "Runs",
"dashboard.find_workspace": "Find workspace...",
"dashboard.workspaces": "Workspaces",
"dashboard.workspaces_empty": "No workspaces yet.",
"dashboard.workspaces_empty_hint": "Create or import a workspace to get started.",
"dashboard.workspaces_no_results": "No matches for \"{query}\".",
"dashboard.workspaces_clear_search": "Clear search",
"dashboard.new_workspace": "New Workspace...",
"dashboard.new_remote_workspace": "Add Remote Workspace...",
"dashboard.forget_workspace": "Forget workspace",
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default {
"dashboard.runs": "运行",
"dashboard.find_workspace": "查找工作区...",
"dashboard.workspaces": "工作区",
"dashboard.workspaces_empty": "还没有工作区。",
"dashboard.workspaces_empty_hint": "创建或导入一个工作区以开始使用。",
"dashboard.workspaces_no_results": "没有匹配“{query}”的工作区。",
"dashboard.workspaces_clear_search": "清除搜索",
"dashboard.new_workspace": "新建工作区...",
"dashboard.new_remote_workspace": "添加远程工作区...",
"dashboard.forget_workspace": "忘记工作区",
Expand Down
Loading