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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### ✨ New Features

- **Prompt library: enqueue to queue** — Add a queue action on prompt cards and an optional "split by line" toggle so multi-line prompts can be queued as separate items. Closes #547.

---

## [1.0.53] - 2026-05-25
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

## [Unreleased]

### ✨ 新功能

- **提示词库:加入队列** — 在提示词卡片上新增加入队列操作,并提供可选的「按行拆分」开关,可将多行提示词拆分为多条队列项。Closes #547。

---

## [1.0.53] - 2026-05-25
Expand Down
17 changes: 17 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ConversationManager } from "~core/conversation-manager"
import { InlineBookmarkManager } from "~core/inline-bookmark-manager"
import { OutlineManager, type OutlineNode } from "~core/outline-manager"
import { AI_STUDIO_SHORTCUT_SYNC_EVENT, PromptManager } from "~core/prompt-manager"
import { QUEUE_ITEMS_ADDED_EVENT } from "~core/queue-inject"
import { QueueDispatcher } from "~core/queue-dispatcher"
import { ensureGlobalThemeManager, type ThemeTransitionOrigin } from "~core/theme-manager"
import { useEdgePeekController } from "~hooks/useEdgePeekController"
Expand Down Expand Up @@ -829,6 +830,22 @@ export const App = () => {
return () => queueDispatcher.stop()
}, [queueDispatcher, settings?.features?.prompts?.promptQueue])

useEffect(() => {
if (!queueDispatcher || !adapter) return

const handleQueueItemsAdded = () => {
if (!queueDispatcher.isRunning()) {
queueDispatcher.start()
}
if (!adapter.isGenerating()) {
void queueDispatcher.processNextNow()
}
}

window.addEventListener(QUEUE_ITEMS_ADDED_EVENT, handleQueueItemsAdded)
return () => window.removeEventListener(QUEUE_ITEMS_ADDED_EVENT, handleQueueItemsAdded)
}, [adapter, queueDispatcher])

const conversationManager = useMemo(() => {
return adapter ? new ConversationManager(adapter) : null
}, [adapter])
Expand Down
130 changes: 126 additions & 4 deletions src/components/PromptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
EyeIcon,
ImportIcon,
PinIcon,
PromptQueueIcon,
SettingsIcon,
TimeIcon,
} from "~components/icons"
Expand All @@ -23,6 +24,7 @@ import {
} from "~components/VariableInputDialog"
import { VIRTUAL_CATEGORY } from "~constants"
import type { PromptManager } from "~core/prompt-manager"
import { injectContentToQueue } from "~core/queue-inject"
import { useSettingsStore } from "~stores/settings-store"
import { APP_NAME } from "~utils/config"
import { t } from "~utils/i18n"
Expand Down Expand Up @@ -85,7 +87,13 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
const submitShortcut = useSettingsStore(
(state) => state.settings.features?.prompts?.submitShortcut ?? "enter",
)

const promptQueueEnabled = useSettingsStore(
(state) => state.settings.features?.prompts?.promptQueue ?? false,
)
const queueInjectSplitByLine = useSettingsStore(
(state) => state.settings.features?.prompts?.queueInjectSplitByLine ?? false,
)
const updateDeepSetting = useSettingsStore((state) => state.updateDeepSetting)
const [prompts, setPrompts] = useState<Prompt[]>([])
const [categories, setCategories] = useState<string[]>([])
const [selectedCategory, setSelectedCategory] = useState<string>(VIRTUAL_CATEGORY.ALL)
Expand Down Expand Up @@ -123,7 +131,14 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
prompt: Prompt | null
variables: ParsedVariable[]
submitAfterInsert: boolean
}>({ show: false, prompt: null, variables: [], submitAfterInsert: false })
enqueueAfterResolve: boolean
}>({
show: false,
prompt: null,
variables: [],
submitAfterInsert: false,
enqueueAfterResolve: false,
})

// 导入确认弹窗状态
const [importDialogState, setImportDialogState] = useState<{
Expand Down Expand Up @@ -184,6 +199,7 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
prompt: targetPrompt,
variables,
submitAfterInsert,
enqueueAfterResolve: false,
})
return true
},
Expand Down Expand Up @@ -366,13 +382,62 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
prompt,
variables,
submitAfterInsert,
enqueueAfterResolve: false,
})
} else {
// No variables; insert (and optionally submit) directly
await doInsert(prompt, prompt.content, submitAfterInsert)
}
}

const doEnqueue = (content: string) => {
const result = injectContentToQueue(content, { splitByLine: queueInjectSplitByLine })
if (result.ok === false) {
if (result.reason === "disabled") {
showToast(
`${t("promptEnqueueDisabled") || "Enable Prompt Queue in settings"}. ${t("queueDisableHint") || ""}`,
3000,
)
return
}
showToast(t("promptEnqueueEmpty") || "Nothing to queue", 2500)
return
}
showToast(
t("promptEnqueueSuccess", { count: String(result.count) }) ||
`Queued ${result.count} prompt(s)`,
2500,
)
}

const handleEnqueueToQueue = (prompt: Prompt, e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()

if (!promptQueueEnabled) {
showToast(
`${t("promptEnqueueDisabled") || "Enable Prompt Queue in settings"}. ${t("queueDisableHint") || ""}`,
3000,
)
return
}

const variables = extractVariables(prompt.content)
if (variables.length > 0) {
setVariableDialogState({
show: true,
prompt,
variables,
submitAfterInsert: false,
enqueueAfterResolve: true,
})
return
}

doEnqueue(prompt.content)
manager.updateLastUsed(prompt.id)
}

const doInsert = async (prompt: Prompt, content: string, submitAfterInsert = false) => {
const success = await manager.insertPrompt(content)
if (success) {
Expand Down Expand Up @@ -404,18 +469,30 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
}

const handleVariableConfirm = async (values: Record<string, string>) => {
const { prompt, submitAfterInsert } = variableDialogState
const { prompt, submitAfterInsert, enqueueAfterResolve } = variableDialogState
if (!prompt) return

const replacedContent = replaceVariables(prompt.content, values)
setVariableDialogState({ show: false, prompt: null, variables: [], submitAfterInsert: false })
setVariableDialogState({
show: false,
prompt: null,
variables: [],
submitAfterInsert: false,
enqueueAfterResolve: false,
})

await new Promise<void>((resolve) => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => resolve())
})
})

if (enqueueAfterResolve) {
doEnqueue(replacedContent)
manager.updateLastUsed(prompt.id)
return
}

await doInsert(prompt, replacedContent, submitAfterInsert)
}

Expand Down Expand Up @@ -634,6 +711,7 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
prompt: null,
variables: [],
submitAfterInsert: false,
enqueueAfterResolve: false,
})
}, [])

Expand Down Expand Up @@ -1547,6 +1625,41 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
<ExportIcon size={16} />
</button>
</Tooltip>
{promptQueueEnabled && (
<Tooltip
content={
t("promptEnqueueSplitQueueTip") ||
"When enabled, each non-empty line in a prompt is added as a separate queue item."
}>
<label
style={{
display: "flex",
alignItems: "center",
gap: "4px",
fontSize: "12px",
color: "var(--gh-text-secondary, #6b7280)",
flexShrink: 0,
cursor: "pointer",
userSelect: "none",
}}
onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={queueInjectSplitByLine}
onChange={(e) =>
updateDeepSetting(
"features",
"prompts",
"queueInjectSplitByLine",
e.target.checked,
)
}
style={{ margin: 0, cursor: "pointer", flexShrink: 0 }}
/>
<span>{t("promptEnqueueSplitQueue") || "Split queue"}</span>
</label>
</Tooltip>
)}
</div>

{/* 分类标签栏:左侧可滚动 + 右侧固定 */}
Expand Down Expand Up @@ -1824,6 +1937,15 @@ export const PromptsTab: React.FC<PromptsTabProps> = ({
<EyeIcon size={16} />
</button>
</Tooltip>
{promptQueueEnabled && (
<Tooltip content={t("promptEnqueueToQueue") || "Add to queue"}>
<button
onClick={(e) => handleEnqueueToQueue(p, e)}
className="prompt-action-btn">
<PromptQueueIcon size={16} />
</button>
</Tooltip>
)}
<Tooltip content={t("copy")}>
<button onClick={(e) => handleCopy(p.content, e)} className="prompt-action-btn">
<CopyIcon size={16} />
Expand Down
39 changes: 1 addition & 38 deletions src/components/QueueOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useSettingsStore } from "~stores/settings-store"
import { useQueueItems, useQueueStore } from "~stores/queue-store"
import { attachEditableKeyboardFocusGuard } from "~utils/dom-toolkit"
import { t } from "~utils/i18n"
import { parseQueueBatchInput, type QueueBatchSplitMode } from "~utils/queue-batch"
import { showToast } from "~utils/toast"

import "~styles/queue-overlay.css"
Expand All @@ -27,46 +28,8 @@ interface QueueOverlayProps {
dispatcher: QueueDispatcher
}

type QueueBatchSplitMode = "line" | "delimiter"

const BATCH_PREVIEW_LIMIT = 5

const normalizeBatchInput = (input: string) => input.replace(/\r\n?/g, "\n")

const decodeQueueDelimiter = (delimiter: string) =>
delimiter.replace(/\\(\\|n|r|t)/g, (_match, token: string) => {
switch (token) {
case "n":
return "\n"
case "r":
return "\r"
case "t":
return "\t"
case "\\":
return "\\"
default:
return token
}
})

const parseQueueBatchInput = (
input: string,
splitMode: QueueBatchSplitMode,
delimiter: string,
): string[] => {
const normalizedInput = normalizeBatchInput(input)
const normalizedDelimiter = normalizeBatchInput(decodeQueueDelimiter(delimiter))

const segments =
splitMode === "line"
? normalizedInput.split("\n")
: normalizedDelimiter
? normalizedInput.split(normalizedDelimiter)
: []

return segments.map((item) => item.trim()).filter(Boolean)
}

export const QueueOverlay: React.FC<QueueOverlayProps> = ({ adapter, dispatcher }) => {
const [isExpanded, setIsExpanded] = useState(false)
const [isGenerating, setIsGenerating] = useState(false)
Expand Down
37 changes: 37 additions & 0 deletions src/core/queue-inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getQueueStore } from "~stores/queue-store"
import { useSettingsStore } from "~stores/settings-store"
import { splitQueueLines } from "~utils/queue-batch"

export const QUEUE_ITEMS_ADDED_EVENT = "ophel:queueItemsAdded"

export type InjectContentToQueueResult =
| { ok: true; count: number }
| { ok: false; reason: "disabled" | "empty" }

export function injectContentToQueue(
content: string,
options: { splitByLine: boolean },
): InjectContentToQueueResult {
const promptQueueEnabled =
useSettingsStore.getState().settings.features?.prompts?.promptQueue ?? false

if (!promptQueueEnabled) {
return { ok: false, reason: "disabled" }
}

const store = getQueueStore()
const items = options.splitByLine
? store.enqueueMany(splitQueueLines(content))
: (() => {
const trimmed = content.trim()
if (!trimmed) return []
return [store.enqueue(trimmed)]
})()

if (items.length === 0) {
return { ok: false, reason: "empty" }
}

window.dispatchEvent(new CustomEvent(QUEUE_ITEMS_ADDED_EVENT))
return { ok: true, count: items.length }
}
7 changes: 7 additions & 0 deletions src/locales/de/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,13 @@ Gib den Code und eine Zusammenfassung der Verbesserungen aus.`,
queueBatchImportAction: "In Warteschlange importieren",
queueBatchImportSuccess: "{count} Prompt(s) importiert",
queueBatchImportEmpty: "Keine Prompts zum Importieren vorhanden",
promptEnqueueToQueue: "Zur Warteschlange hinzufügen",
promptEnqueueSplitQueue: "Warteschlange aufteilen",
promptEnqueueSplitQueueTip:
"Wenn aktiviert, wird jede nicht leere Zeile eines Prompts als separater Warteschlangeneintrag hinzugefügt.",
promptEnqueueSuccess: "{count} Prompt(s) in die Warteschlange gestellt",
promptEnqueueDisabled: "Aktivieren Sie die Prompt-Warteschlange in den Einstellungen",
promptEnqueueEmpty: "Nichts zum Einreihen vorhanden",
zenModeTitle: "Zen-Modus",
zenModeLabel: "Zen-Modus aktivieren",
zenModeDesc:
Expand Down
7 changes: 7 additions & 0 deletions src/locales/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,13 @@ Please output the optimized code block directly, followed by a brief summary of
queueBatchImportAction: "Import to Queue",
queueBatchImportSuccess: "Imported {count} prompt(s)",
queueBatchImportEmpty: "No prompts available to import",
promptEnqueueToQueue: "Add to queue",
promptEnqueueSplitQueue: "Split queue",
promptEnqueueSplitQueueTip:
"When enabled, each non-empty line in a prompt is added as a separate queue item.",
promptEnqueueSuccess: "Queued {count} prompt(s)",
promptEnqueueDisabled: "Enable Prompt Queue in settings",
promptEnqueueEmpty: "Nothing to queue",
zenModeTitle: "Zen Mode",
zenModeLabel: "Enable Zen Mode",
zenModeDesc:
Expand Down
Loading
Loading