mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 11:48:42 +02:00
Compare commits
10 Commits
fix/deskto
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e91f1f40 | ||
|
|
50acd48992 | ||
|
|
cb329c5475 | ||
|
|
5e1c7878d6 | ||
|
|
6a665c68a3 | ||
|
|
174b8c62a6 | ||
|
|
768d3f8b0c | ||
|
|
7dfa72465c | ||
|
|
0b969483a6 | ||
|
|
e024ab1232 |
33
apps/desktop/src/main/app-version.ts
Normal file
33
apps/desktop/src/main/app-version.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { app } from "electron";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Resolve the running app version. In packaged builds this is the value
|
||||
* `electron-builder` baked into package.json via `extraMetadata.version`
|
||||
* (driven by `git describe` — see `apps/desktop/scripts/package.mjs`), so
|
||||
* `app.getVersion()` matches the GitHub Release tag exactly.
|
||||
*
|
||||
* In dev (`pnpm dev:desktop`) `app.getVersion()` only sees the static
|
||||
* `apps/desktop/package.json` value, which is "0.1.0" and never bumped —
|
||||
* the Settings → Updates panel and any other UI surfacing the version
|
||||
* would mislead developers into thinking they're running ancient builds.
|
||||
* Fall back to `git describe --tags --always --dirty` (same source the
|
||||
* packager uses) so dev shows e.g. `0.2.19-14-gabcdef-dirty`. If git is
|
||||
* unavailable for whatever reason, we just return the package.json value.
|
||||
*/
|
||||
export function getAppVersion(): string {
|
||||
if (app.isPackaged) {
|
||||
return app.getVersion();
|
||||
}
|
||||
try {
|
||||
const raw = execSync("git describe --tags --always --dirty", {
|
||||
cwd: app.getAppPath(),
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
if (!raw) return app.getVersion();
|
||||
return raw.replace(/^v/, "");
|
||||
} catch {
|
||||
return app.getVersion();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { setupAutoUpdater } from "./updater";
|
||||
import { setupDaemonManager } from "./daemon-manager";
|
||||
import { openExternalSafely } from "./external-url";
|
||||
import { installContextMenu } from "./context-menu";
|
||||
import { getAppVersion } from "./app-version";
|
||||
|
||||
// Bundled icon used for dev-mode dock/taskbar branding. In production the
|
||||
// app bundle icon (from electron-builder) wins; this path is only consumed
|
||||
@@ -203,7 +204,7 @@ if (!gotTheLock) {
|
||||
ipcMain.on("app:get-info", (event) => {
|
||||
const p = process.platform;
|
||||
const os = p === "darwin" ? "macos" : p === "win32" ? "windows" : p === "linux" ? "linux" : "unknown";
|
||||
event.returnValue = { version: app.getVersion(), os };
|
||||
event.returnValue = { version: getAppVersion(), os };
|
||||
});
|
||||
|
||||
// IPC: toggle immersive mode — hides the macOS traffic lights so full-screen
|
||||
|
||||
@@ -15,6 +15,8 @@ import { defaultStorage } from "../../platform/storage";
|
||||
interface QuickCreateState {
|
||||
lastAgentId: string | null;
|
||||
setLastAgentId: (id: string | null) => void;
|
||||
keepOpen: boolean;
|
||||
setKeepOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const useQuickCreateStore = create<QuickCreateState>()(
|
||||
@@ -22,6 +24,8 @@ export const useQuickCreateStore = create<QuickCreateState>()(
|
||||
(set) => ({
|
||||
lastAgentId: null,
|
||||
setLastAgentId: (id) => set({ lastAgentId: id }),
|
||||
keepOpen: false,
|
||||
setKeepOpen: (v) => set({ keepOpen: v }),
|
||||
}),
|
||||
{
|
||||
name: "multica_quick_create",
|
||||
|
||||
@@ -88,6 +88,11 @@ export function InboxDetailLabel({ item }: { item: InboxItem }) {
|
||||
if (emoji) return <span>Reacted {emoji} to your comment</span>;
|
||||
return <span>{typeLabels[item.type]}</span>;
|
||||
}
|
||||
case "quick_create_done": {
|
||||
const identifier = details.identifier;
|
||||
if (identifier) return <span>Created {identifier}</span>;
|
||||
return <span>{typeLabels[item.type]}</span>;
|
||||
}
|
||||
default:
|
||||
return <span>{typeLabels[item.type] ?? item.type}</span>;
|
||||
}
|
||||
|
||||
@@ -399,14 +399,6 @@ describe("IssueDetail (shared)", () => {
|
||||
expect(screen.getByDisplayValue("Add JWT auth to the backend")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders issue identifier in the breadcrumb", async () => {
|
||||
renderIssueDetail();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("TES-1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders workspace name as breadcrumb link", async () => {
|
||||
renderIssueDetail();
|
||||
|
||||
|
||||
@@ -506,9 +506,6 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
|
||||
</>
|
||||
)}
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
{issue.identifier}
|
||||
</span>
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{issue.title}
|
||||
</span>
|
||||
|
||||
@@ -560,7 +560,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="text-muted-foreground"
|
||||
onClick={() => useModalStore.getState().open("create-issue")}
|
||||
onClick={() => useModalStore.getState().open("quick-create-issue")}
|
||||
>
|
||||
<span className="relative">
|
||||
<SquarePen />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ArrowLeftRight, ChevronRight, X as XIcon } from "lucide-react";
|
||||
import { ArrowLeftRight, Check, ChevronRight, X as XIcon } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { DialogTitle } from "@multica/ui/components/ui/dialog";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@multica/ui/components/ui/dropdown-menu";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Switch } from "@multica/ui/components/ui/switch";
|
||||
import { api, ApiError } from "@multica/core/api";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { useCurrentWorkspace } from "@multica/core/paths";
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
useFileDropZone,
|
||||
FileDropOverlay,
|
||||
} from "../editor";
|
||||
import { FileUploadButton } from "@multica/ui/components/common/file-upload-button";
|
||||
|
||||
// AgentCreatePanel — agent-mode body of the create-issue dialog. Renders
|
||||
// only the inner content; the surrounding `<Dialog>` AND `<DialogContent>`
|
||||
@@ -78,6 +80,8 @@ export function AgentCreatePanel({
|
||||
|
||||
const lastAgentId = useQuickCreateStore((s) => s.lastAgentId);
|
||||
const setLastAgentId = useQuickCreateStore((s) => s.setLastAgentId);
|
||||
const keepOpen = useQuickCreateStore((s) => s.keepOpen);
|
||||
const setKeepOpen = useQuickCreateStore((s) => s.setKeepOpen);
|
||||
const setLastMode = useCreateModeStore((s) => s.setLastMode);
|
||||
|
||||
const [agentId, setAgentId] = useState<string | undefined>(() => {
|
||||
@@ -130,12 +134,14 @@ export function AgentCreatePanel({
|
||||
const editorRef = useRef<ContentEditorRef>(null);
|
||||
const [hasContent, setHasContent] = useState(initialPrompt.trim().length > 0);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [justSent, setJustSent] = useState(false);
|
||||
const [sentCount, setSentCount] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Image paste/drop support: route uploads through the same helper Advanced
|
||||
// uses, so users can paste screenshots straight into the prompt and the
|
||||
// agent receives them as embedded markdown image URLs in the prompt.
|
||||
const { uploadWithToast } = useFileUpload(api);
|
||||
const { uploadWithToast, uploading } = useFileUpload(api);
|
||||
const handleUploadFile = useCallback(
|
||||
(file: File) => uploadWithToast(file),
|
||||
[uploadWithToast],
|
||||
@@ -154,7 +160,7 @@ export function AgentCreatePanel({
|
||||
|
||||
const submit = async () => {
|
||||
const md = editorRef.current?.getMarkdown()?.trim() ?? "";
|
||||
if (!md || !agentId || submitting || versionBlocked) return;
|
||||
if (!md || !agentId || submitting || versionBlocked || uploading) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -164,7 +170,18 @@ export function AgentCreatePanel({
|
||||
toast.success("Sent to agent — you'll get an inbox notification when it's done", {
|
||||
duration: 4000,
|
||||
});
|
||||
onClose();
|
||||
if (keepOpen) {
|
||||
// Stay open for continuous creation — clear the editor so the
|
||||
// user can immediately type the next prompt.
|
||||
editorRef.current?.clearContent();
|
||||
setHasContent(false);
|
||||
setSentCount((c) => c + 1);
|
||||
setJustSent(true);
|
||||
setTimeout(() => setJustSent(false), 1500);
|
||||
requestAnimationFrame(() => editorRef.current?.focus());
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
} catch (e) {
|
||||
// Server returns 422 with { code, ... } for the structured rejection
|
||||
// paths the modal cares about. Surface the reason in-modal so the
|
||||
@@ -334,7 +351,18 @@ export function AgentCreatePanel({
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t shrink-0">
|
||||
<span className="text-xs text-muted-foreground">⌘↵ to submit</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FileUploadButton
|
||||
size="sm"
|
||||
onSelect={(file) => editorRef.current?.uploadFile(file)}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{keepOpen && sentCount > 0 && (
|
||||
<span className="text-emerald-600 dark:text-emerald-400">{sentCount} sent · </span>
|
||||
)}
|
||||
⌘↵ to submit
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -345,17 +373,28 @@ export function AgentCreatePanel({
|
||||
<ArrowLeftRight className="size-3.5" />
|
||||
Switch to manual
|
||||
</button>
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
|
||||
<Switch
|
||||
size="sm"
|
||||
checked={keepOpen}
|
||||
onCheckedChange={setKeepOpen}
|
||||
/>
|
||||
Create another
|
||||
</label>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={submit}
|
||||
disabled={!hasContent || !agentId || submitting || versionBlocked}
|
||||
disabled={!hasContent || !agentId || submitting || versionBlocked || uploading}
|
||||
title={
|
||||
versionBlocked
|
||||
? `Daemon CLI must be ≥ ${versionCheck.min}`
|
||||
: undefined
|
||||
}
|
||||
className={justSent ? "!bg-emerald-600 !text-white" : undefined}
|
||||
>
|
||||
{submitting ? "Sending…" : "Create"}
|
||||
{submitting ? "Sending…" : uploading ? "Uploading…" : justSent ? (
|
||||
<span className="flex items-center gap-1"><Check className="size-3.5" />Sent</span>
|
||||
) : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -261,7 +261,7 @@ describe("SearchCommand", () => {
|
||||
);
|
||||
await user.click(newIssue);
|
||||
|
||||
expect(mockOpenModal).toHaveBeenCalledWith("create-issue");
|
||||
expect(mockOpenModal).toHaveBeenCalledWith("quick-create-issue");
|
||||
expect(useSearchStore.getState().open).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ export function SearchCommand() {
|
||||
icon: Plus,
|
||||
keywords: ["new", "issue", "create", "add"],
|
||||
onSelect: () => {
|
||||
useModalStore.getState().open("create-issue");
|
||||
useModalStore.getState().open("quick-create-issue");
|
||||
setOpen(false);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1436,7 +1436,7 @@ func (s *TaskService) notifyQuickCreateCompleted(ctx context.Context, task db.Ag
|
||||
Type: "quick_create_done",
|
||||
Severity: "info",
|
||||
IssueID: issue.ID,
|
||||
Title: fmt.Sprintf("Created %s: %s", identifier, issue.Title),
|
||||
Title: issue.Title,
|
||||
Body: pgtype.Text{},
|
||||
ActorType: pgtype.Text{String: "agent", Valid: true},
|
||||
ActorID: task.AgentID,
|
||||
|
||||
Reference in New Issue
Block a user