mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-19 20:58:56 +02:00
Compare commits
2 Commits
fix/deskto
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc0d265d8b | ||
|
|
33990ee430 |
@@ -1,33 +0,0 @@
|
||||
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,7 +7,6 @@ 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
|
||||
@@ -204,7 +203,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: getAppVersion(), os };
|
||||
event.returnValue = { version: app.getVersion(), os };
|
||||
});
|
||||
|
||||
// IPC: toggle immersive mode — hides the macOS traffic lights so full-screen
|
||||
|
||||
@@ -5,13 +5,12 @@ import { Button } from "@multica/ui/components/ui/button";
|
||||
type CheckState =
|
||||
| { status: "idle" }
|
||||
| { status: "checking" }
|
||||
| { status: "up-to-date" }
|
||||
| { status: "up-to-date"; currentVersion: string }
|
||||
| { status: "available"; latestVersion: string }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
export function UpdatesSettingsTab() {
|
||||
const [state, setState] = useState<CheckState>({ status: "idle" });
|
||||
const currentVersion = window.desktopAPI.appInfo.version;
|
||||
|
||||
const handleCheck = useCallback(async () => {
|
||||
setState({ status: "checking" });
|
||||
@@ -23,7 +22,7 @@ export function UpdatesSettingsTab() {
|
||||
setState(
|
||||
result.available
|
||||
? { status: "available", latestVersion: result.latestVersion }
|
||||
: { status: "up-to-date" },
|
||||
: { status: "up-to-date", currentVersion: result.currentVersion },
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -36,15 +35,6 @@ export function UpdatesSettingsTab() {
|
||||
</p>
|
||||
|
||||
<div className="mt-6 divide-y">
|
||||
<div className="flex items-center justify-between gap-6 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">Current version</p>
|
||||
<p className="text-sm text-muted-foreground mt-0.5 font-mono">
|
||||
v{currentVersion}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-6 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">Check for updates</p>
|
||||
@@ -55,7 +45,7 @@ export function UpdatesSettingsTab() {
|
||||
{state.status === "up-to-date" && (
|
||||
<p className="text-sm text-muted-foreground mt-2 inline-flex items-center gap-1.5">
|
||||
<Check className="size-3.5 text-success" />
|
||||
You're on the latest version.
|
||||
You're on the latest version (v{state.currentVersion}).
|
||||
</p>
|
||||
)}
|
||||
{state.status === "available" && (
|
||||
|
||||
@@ -39,7 +39,6 @@ import { BaseMentionExtension } from "./mention-extension";
|
||||
import { createMentionSuggestion } from "./mention-suggestion";
|
||||
import { CodeBlockView } from "./code-block-view";
|
||||
import { createMarkdownPasteExtension } from "./markdown-paste";
|
||||
import { createMarkdownCopyExtension } from "./markdown-copy";
|
||||
import { createSubmitExtension } from "./submit-shortcut";
|
||||
import { createBlurShortcutExtension } from "./blur-shortcut";
|
||||
import { createFileUploadExtension } from "./file-upload";
|
||||
@@ -130,10 +129,6 @@ export function createEditorExtensions(
|
||||
InlineMathExtension,
|
||||
// 3-space indent so nested ordered lists survive CommonMark in ReadonlyContent.
|
||||
Markdown.configure({ indentation: { style: "space", size: 3 } }),
|
||||
// Make Cmd+C / Cmd+X / drag write Markdown source to clipboard text/plain.
|
||||
// Registered for both editable and readonly so users can copy from rendered
|
||||
// comments and paste the original Markdown elsewhere.
|
||||
createMarkdownCopyExtension(),
|
||||
FileCardExtension,
|
||||
...(options.disableMentions
|
||||
? []
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Markdown copy extension — make the clipboard's text/plain channel carry
|
||||
* Markdown source instead of plain textContent.
|
||||
*
|
||||
* Symmetric to markdown-paste.ts:
|
||||
* paste: text/plain → editor.markdown.parse → doc
|
||||
* copy: slice → editor.markdown.serialize → text/plain
|
||||
*
|
||||
* Why: ProseMirror's default clipboardTextSerializer calls Slice.textBetween,
|
||||
* which flattens every node to its inner text. Headings, lists, code blocks,
|
||||
* mentions, file cards — all lose their Markdown markers. Pasting into VS
|
||||
* Code, terminals, or messaging apps then sees only naked text.
|
||||
*
|
||||
* The text/html channel is left at ProseMirror's default so pasting back
|
||||
* into another ProseMirror editor still preserves exact node structure via
|
||||
* data-pm-slice.
|
||||
*/
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import type { Slice } from "@tiptap/pm/model";
|
||||
|
||||
// Blob URLs (blob:http://…) are process-local; never let them leave the page.
|
||||
const BLOB_IMAGE_RE = /!\[[^\]]*\]\(blob:[^)]*\)\n?/g;
|
||||
|
||||
export function createMarkdownCopyExtension() {
|
||||
return Extension.create({
|
||||
name: "markdownCopy",
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
|
||||
const fallback = (slice: Slice) =>
|
||||
slice.content.textBetween(0, slice.content.size, "\n\n");
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("markdownCopy"),
|
||||
props: {
|
||||
clipboardTextSerializer(slice: Slice) {
|
||||
if (!editor.markdown) return fallback(slice);
|
||||
try {
|
||||
// Wrap slice content in a temp doc so the serializer walks
|
||||
// it like a real document. Inline-only slices auto-wrap
|
||||
// into doc → paragraph; block slices pass through.
|
||||
const doc = editor.schema.topNodeType.create(
|
||||
null,
|
||||
slice.content,
|
||||
);
|
||||
const md = editor.markdown.serialize(doc.toJSON());
|
||||
return md.replace(BLOB_IMAGE_RE, "").replace(/\n+$/, "");
|
||||
} catch {
|
||||
// Special selections (e.g. table cellSelection) may fail
|
||||
// schema validation when wrapped in a doc node. Fall back
|
||||
// so copy never breaks.
|
||||
return fallback(slice);
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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, ChevronRight, Sparkles, X as XIcon } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { DialogTitle } from "@multica/ui/components/ui/dialog";
|
||||
@@ -253,6 +253,7 @@ export function AgentCreatePanel({
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer rounded-sm px-1.5 py-1 -ml-1.5 hover:bg-accent/60"
|
||||
>
|
||||
<Sparkles className="size-3.5" />
|
||||
<span>Created by</span>
|
||||
{selectedAgent ? (
|
||||
<span className="flex items-center gap-1.5 text-foreground">
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user