Compare commits

..

2 Commits

Author SHA1 Message Date
Jiayuan
bc0d265d8b test(search): update search-command test to expect quick-create-issue
Aligns the test assertion with the behavior change in the previous
commit where "New Issue" now opens Quick Capture.
2026-04-29 13:18:51 +02:00
Jiayuan
33990ee430 feat(ui): make New Issue button open Quick Capture instead of manual form
The sidebar "New Issue" button and the search command's "New Issue" action
now open the agent-based Quick Capture dialog directly, matching the
platform's agent-first workflow.

Contextual issue creation (board columns, list view status groups, sub-issues)
still opens the manual form since those pass pre-filled data.

Closes MUL-1558
2026-04-29 12:51:45 +02:00
9 changed files with 9 additions and 119 deletions

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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&apos;re on the latest version.
You&apos;re on the latest version (v{state.currentVersion}).
</p>
)}
{state.status === "available" && (

View File

@@ -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
? []

View File

@@ -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);
}
},
},
}),
];
},
});
}

View File

@@ -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 />

View File

@@ -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">

View File

@@ -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);
});

View File

@@ -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);
},
},