feat: agent and CLI control plane for sessions, worktrees, and scheduled tasks (#2408)

Add a shared OpenChamber control service with two thin adapters — a native
`openchamber` tool injected into managed OpenCode, and new CLI commands — so
users can manage parallel sessions, worktrees, and scheduled tasks
conversationally through agents or from the terminal.

Control plane:
- New openchamber-control service owning a fixed action contract:
  projects.list, models.list, session list/create/send/fork/status/messages,
  and schedule list/create/run/delete/toggle. Session and worktree deletion
  and project registration are deliberately not exposed.
- New openchamber-sessions module owning create/worktree/prompt orchestration,
  Goal Mode dispatch, wait semantics (initial idle never counts as completion;
  timeout and cancellation are failures), and explicit partial-failure results.
- Scheduled-task logic extracted into a service shared by routes, CLI, and the
  agent tool.

Agent tool:
- Managed OpenCode gets a materialized plugin registering one typed tool with
  a loopback-only callback, per-child ephemeral bearer (timing-safe, never
  persisted or logged), and abort propagation into the service.
- The ~1.5k-token schema applies progressive disclosure: short descriptions,
  server-side validation returning actionable usage errors, and intent
  guardrails — created sessions/tasks are user-facing work (not age
  self-delegation); worktree/goal/agent/variant/wait are omit-by-default;
  dispatches produce no completion notification, and later result r
  to session.messages, which now returns the authoritative sessionStatus.
- session.create without a user-named model picks from favorites/re
  send/fork omit the selection and the service reuses the target session's
  last user-message model, agent, and variant before falling back t
- An "Agent control tool" setting (default on, Save + Reload to apply)
  disables plugin injection entirely.

CLI:
- New `openchamber session`, `schedule`, `projects`, and `models` commands
  with automatic instance targeting, --wait/--timeout/--last-assist
  worktree flags, and Goal Mode, preserving interactive, non-TTY, --quiet,
  and --json contracts. The control HTTP timeout derives from the w
  instead of the 4-second default.

UI:
- New built-in "Schedule a Task" starter (/schedule-task) running a
  dialogue that defines a task and offers to create it via the tool after
  explicit confirmation; Craft a Goal and Feature Planning gain the
  handoff offer, and guided starters reserve the question tool for concrete
  option choices. Localized in all 10 locales, migrated into custom
  starter lists, hidden on VS Code.
- Sidebar shows CLI/agent-created sessions live via the control eve
- openchamber tool calls render with per-action titles and metadata.
This commit is contained in:
Bohdan Triapitsyn
2026-07-24 21:54:28 +03:00
committed by GitHub
parent 484fe8bc18
commit e908db637b
85 changed files with 5375 additions and 298 deletions
+27 -1
View File
@@ -1288,7 +1288,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
const availableSkills = useSkillsStore((s) => s.skills); const availableSkills = useSkillsStore((s) => s.skills);
const knownSlashNames = React.useMemo(() => { const knownSlashNames = React.useMemo(() => {
const names = new Set<string>([ const names = new Set<string>([
'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'summary', 'workspace-review', 'plan-feature', 'craft-goal', 'catch-up', 'debug', 'weigh', 'explore', 'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'summary', 'workspace-review', 'plan-feature', 'craft-goal', 'schedule-task', 'catch-up', 'debug', 'weigh', 'explore',
]); ]);
if (!isMobile && !isVSCodeRuntime()) names.add('handoff-review'); if (!isMobile && !isVSCodeRuntime()) names.add('handoff-review');
for (const command of availableCommands) names.add(command.name.toLowerCase()); for (const command of availableCommands) names.add(command.name.toLowerCase());
@@ -2257,6 +2257,32 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
} }
return; return;
} }
else if (commandName === 'schedule-task' && (currentSessionId || newSessionDraftOpen)) {
try {
await sessionActions.waitForConnectionOrThrow();
const idea = normalizedCommand.replace(/^\/schedule-task\b/i, '').trim();
const visibleText = await renderMagicPrompt('session.scheduleTask.visible', {
idea_block: idea ? `\n\nHere is my initial idea:\n${idea}` : '',
});
const instructionsText = await renderMagicPrompt('session.scheduleTask.instructions');
await sendMessage(
visibleText,
providerIdToSend,
modelIdToSend,
agentNameToSend,
[],
agentMentionName,
[{ text: instructionsText, synthetic: true }],
variantToSend,
inputMode,
sendMessageOptions,
);
scrollToBottom?.();
} catch (error) {
toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.scheduleTaskFailed'));
}
return;
}
else if (commandName === 'catch-up' && (currentSessionId || newSessionDraftOpen)) { else if (commandName === 'catch-up' && (currentSessionId || newSessionDraftOpen)) {
try { try {
await sessionActions.waitForConnectionOrThrow(); await sessionActions.waitForConnectionOrThrow();
@@ -170,6 +170,10 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
? [{ id: 'openchamber:craft-goal', name: 'craft-goal', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.craftGoalDescription'), isOpenChamber: true }] ? [{ id: 'openchamber:craft-goal', name: 'craft-goal', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.craftGoalDescription'), isOpenChamber: true }]
: [] : []
), ),
...(canStartSessionCommand
? [{ id: 'openchamber:schedule-task', name: 'schedule-task', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.scheduleTaskDescription'), isOpenChamber: true }]
: []
),
...(canStartSessionCommand ...(canStartSessionCommand
? [{ id: 'openchamber:catch-up', name: 'catch-up', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.catchUpDescription'), isOpenChamber: true }] ? [{ id: 'openchamber:catch-up', name: 'catch-up', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.catchUpDescription'), isOpenChamber: true }]
: [] : []
@@ -243,6 +247,10 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
? [{ id: 'openchamber:craft-goal', name: 'craft-goal', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.craftGoalDescription'), isOpenChamber: true }] ? [{ id: 'openchamber:craft-goal', name: 'craft-goal', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.craftGoalDescription'), isOpenChamber: true }]
: [] : []
), ),
...(canStartSessionCommand
? [{ id: 'openchamber:schedule-task', name: 'schedule-task', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.scheduleTaskDescription'), isOpenChamber: true }]
: []
),
...(canStartSessionCommand ...(canStartSessionCommand
? [{ id: 'openchamber:catch-up', name: 'catch-up', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.catchUpDescription'), isOpenChamber: true }] ? [{ id: 'openchamber:catch-up', name: 'catch-up', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.catchUpDescription'), isOpenChamber: true }]
: [] : []
@@ -53,6 +53,7 @@ Use this doc when you ask an agent to change tool/header/description behavior.
- `read` and `skill` are **static navigation tools** and render via `StaticToolRow`. - `read` and `skill` are **static navigation tools** and render via `StaticToolRow`.
- Every other tool, including search/fetch, OpenCode built-ins, custom tools, plugins, and MCP tools, is **expandable** and renders through `ToolPart`. - Every other tool, including search/fetch, OpenCode built-ins, custom tools, plugins, and MCP tools, is **expandable** and renders through `ToolPart`.
- The managed `openchamber` plugin tool uses the expandable path and hides its broad protocol input. The plugin supplies the selected action's human description as the native tool title; the UI renders that metadata without owning an action map. The full versioned result envelope renders through the same neutral JSON summary/tree/raw views as other tools, without a tool-specific output card.
- `ToolPart` defers expanded content after a user toggle, preventing large tool input/output payloads from mounting during the initial chat render. - `ToolPart` defers expanded content after a user toggle, preventing large tool input/output payloads from mounting during the initial chat render.
- Thinking/Justification duration is hidden in `sorted` mode (handled in `ReasoningPart.tsx` + `JustificationBlock.tsx`). - Thinking/Justification duration is hidden in `sorted` mode (handled in `ReasoningPart.tsx` + `JustificationBlock.tsx`).
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test'; import { describe, expect, test } from 'bun:test';
import { readTaskTagSessionIdFromOutput } from './taskSessionIdParser'; import { readTaskTagSessionIdFromOutput } from './taskSessionIdParser';
import { tryParseJsonOutput } from '../toolRenderers';
describe('readTaskTagSessionIdFromOutput', () => { describe('readTaskTagSessionIdFromOutput', () => {
test('parses task tags without state attributes', () => { test('parses task tags without state attributes', () => {
@@ -11,3 +12,15 @@ describe('readTaskTagSessionIdFromOutput', () => {
expect(readTaskTagSessionIdFromOutput('<task id="ses_def456" state="completed">')).toBe('ses_def456'); expect(readTaskTagSessionIdFromOutput('<task id="ses_def456" state="completed">')).toBe('ses_def456');
}); });
}); });
describe('OpenChamber tool output', () => {
test('keeps the result envelope in the generic JSON rendering pipeline', () => {
const result = {
schemaVersion: 1,
ok: true,
action: 'projects.list',
data: { projects: [] },
};
expect(tryParseJsonOutput(JSON.stringify(result))).toEqual({ data: result, isJson: true });
});
});
@@ -971,7 +971,6 @@ const ToolScrollableTextOutput: React.FC<{
ToolScrollableTextOutput.displayName = 'ToolScrollableTextOutput'; ToolScrollableTextOutput.displayName = 'ToolScrollableTextOutput';
const getTaskSummaryLabel = (entry: TaskToolSummaryEntry): string => { const getTaskSummaryLabel = (entry: TaskToolSummaryEntry): string => {
const title = entry.state?.title; const title = entry.state?.title;
if (typeof title === 'string' && title.trim().length > 0) { if (typeof title === 'string' && title.trim().length > 0) {
@@ -1514,7 +1513,6 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
const hasStringOutput = typeof rawOutput === 'string' && rawOutput.length > 0; const hasStringOutput = typeof rawOutput === 'string' && rawOutput.length > 0;
const outputString = typeof rawOutput === 'string' ? rawOutput : ''; const outputString = typeof rawOutput === 'string' ? rawOutput : '';
const attachments = stateWithData.attachments; const attachments = stateWithData.attachments;
const fileDiff = isRecord(metadata?.filediff) ? metadata.filediff : undefined; const fileDiff = isRecord(metadata?.filediff) ? metadata.filediff : undefined;
const diffContent = getPatchText((metadata as { patch?: unknown } | undefined)?.patch) const diffContent = getPatchText((metadata as { patch?: unknown } | undefined)?.patch)
?? getPatchText(metadata?.diff) ?? getPatchText(metadata?.diff)
@@ -1526,7 +1524,8 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
[currentDirectory, diffContent, metadata] [currentDirectory, diffContent, metadata]
); );
const hasVisualDiffEntry = diffEntries.some((entry) => entry.renderMode === 'diff'); const hasVisualDiffEntry = diffEntries.some((entry) => entry.renderMode === 'diff');
const hideToolInputPreview = part.tool === 'apply_patch' const hideToolInputPreview = part.tool === 'openchamber'
|| part.tool === 'apply_patch'
|| part.tool === 'edit' || part.tool === 'edit'
|| part.tool === 'multiedit'; || part.tool === 'multiedit';
const diagnosticSection = React.useMemo( const diagnosticSection = React.useMemo(
@@ -53,6 +53,9 @@ export const getToolIcon = (toolName: string) => {
if (tool === 'task') { if (tool === 'task') {
return <Icon name="ai-agent" className={iconClass} />; return <Icon name="ai-agent" className={iconClass} />;
} }
if (tool === 'openchamber') {
return <Icon name="openchamber" className={iconClass} />;
}
if (tool === 'question') { if (tool === 'question') {
return <Icon name="survey" className={iconClass} />; return <Icon name="survey" className={iconClass} />;
} }
@@ -107,7 +107,7 @@ export function useDraftStarters(): UseDraftStartersResult {
const skillNames = React.useMemo(() => new Set(skills.map((s) => s.name)), [skills]); const skillNames = React.useMemo(() => new Set(skills.map((s) => s.name)), [skills]);
const resolve = React.useCallback((ref: DraftStarterRef, group: StarterGroup): ResolvedStarter | null => { const resolve = React.useCallback((ref: DraftStarterRef, group: StarterGroup): ResolvedStarter | null => {
if (isVSCode && ref.type === 'command' && ref.name === 'craft-goal') return null; if (isVSCode && ref.type === 'command' && (ref.name === 'craft-goal' || ref.name === 'schedule-task')) return null;
if (ref.type === 'command') { if (ref.type === 'command') {
const builtin = getBuiltInStarter(ref.name); const builtin = getBuiltInStarter(ref.name);
if (builtin) { if (builtin) {
@@ -144,7 +144,7 @@ export function useDraftStarters(): UseDraftStartersResult {
const pinnable = React.useMemo<PinnableItem[]>(() => { const pinnable = React.useMemo<PinnableItem[]>(() => {
const items: PinnableItem[] = []; const items: PinnableItem[] = [];
for (const b of BUILTIN_STARTERS) { for (const b of BUILTIN_STARTERS) {
if (isVSCode && b.name === 'craft-goal') continue; if (isVSCode && (b.name === 'craft-goal' || b.name === 'schedule-task')) continue;
items.push({ type: 'command', name: b.name, label: t(b.labelKey), icon: b.icon, section: 'built-in', scope: 'user' }); items.push({ type: 'command', name: b.name, label: t(b.labelKey), icon: b.icon, section: 'built-in', scope: 'user' });
} }
for (const c of commands) { for (const c of commands) {
+3 -1
View File
@@ -1,6 +1,6 @@
# Icon System # Icon System
OpenChamber uses an SVG sprite-based icon system for performance. All icons are rendered via a single hidden SVG sprite injected into the DOM, referenced by `<use href="#oc-icon-name"/>`. OpenChamber uses an SVG sprite-based icon system for performance. Remixicon glyphs and explicitly registered custom glyphs are rendered via a single hidden SVG sprite injected into the DOM, referenced by `<use href="#oc-icon-name"/>`.
## Usage ## Usage
@@ -31,6 +31,8 @@ Common suffixes:
If the icon doesn't exist in the sprite, the script will warn you. If the icon doesn't exist in the sprite, the script will warn you.
Custom product glyphs are registered in `scripts/generate-icon-sprite.mjs`. They must use the shared `24x24` viewbox and `currentColor` so they match Remixicon sizing and theme behavior.
## Sizing ## Sizing
The `Icon` component does not have a `size` prop. Use Tailwind classes instead: The `Icon` component does not have a `size` prop. Use Tailwind classes instead:
+40 -39
View File
@@ -2,10 +2,10 @@
// Do not edit manually. Run the script to update. // Do not edit manually. Run the script to update.
export const iconSpriteData = { export const iconSpriteData = {
"add-circle": `<path d="M11 11V7H13V11H17V13H13V17H11V13H7V11H11ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z" fill="currentColor"/>`,
"add": `<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" fill="currentColor"/>`, "add": `<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" fill="currentColor"/>`,
"ai-agent-fill": `<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 15C9.71266 15 7.65042 15.961 6.19238 17.5C7.65042 19.039 9.71266 20 12 20C14.2871 20 16.3486 19.0387 17.8066 17.5C16.3486 15.9613 14.2871 15 12 15ZM12.4707 5.31934C12.2943 4.89337 11.7058 4.89339 11.5293 5.31934L11.2764 5.93066C10.8445 6.97341 10.0384 7.80621 9.02539 8.25684L8.30762 8.57617C7.89751 8.75905 7.89744 9.35625 8.30762 9.53906L9.06738 9.87695C10.0551 10.3163 10.8476 11.1193 11.2871 12.1279L11.5332 12.6934C11.7138 13.1073 12.2863 13.1073 12.4668 12.6934L12.7139 12.1279C13.1534 11.1194 13.9449 10.3163 14.9326 9.87695L15.6924 9.53906C16.1026 9.35624 16.1025 8.75907 15.6924 8.57617L14.9746 8.25684C13.9616 7.8062 13.1556 6.9734 12.7236 5.93066L12.4707 5.31934Z" fill="currentColor"/>`, "add-circle": `<path d="M11 11V7H13V11H17V13H13V17H11V13H7V11H11ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z" fill="currentColor"/>`,
"ai-agent": `<path d="M12 2C17.5228 2 22 6.47715 22 12C22 14.7096 20.9205 17.1697 19.1709 18.9697C17.3551 20.8376 14.8124 22 12 22C9.18756 22 6.64488 20.8376 4.8291 18.9697C3.07949 17.1697 2 14.7096 2 12C2 6.47715 6.47715 2 12 2ZM12 16C10.0022 16 8.20124 16.8375 6.9248 18.1816C8.30642 19.3175 10.0724 20 12 20C13.9274 20 15.6927 19.3173 17.0742 18.1816C15.7978 16.8377 13.9975 16 12 16ZM12 4C7.58172 4 4 7.58172 4 12C4 13.7701 4.57462 15.4044 5.54785 16.7295C7.1822 15.0483 9.46797 14 12 14C14.5318 14 16.8169 15.0485 18.4512 16.7295C19.4246 15.4043 20 13.7703 20 12C20 7.58172 16.4183 4 12 4ZM11.5293 5.31934C11.7058 4.89329 12.2943 4.89329 12.4707 5.31934L12.7236 5.93066C13.1556 6.97343 13.9615 7.80622 14.9746 8.25684L15.6924 8.5752C16.1029 8.75796 16.1028 9.35627 15.6924 9.53906L14.9326 9.87695C13.9448 10.3163 13.1534 11.1193 12.7139 12.1279L12.4668 12.6934C12.2864 13.1074 11.7137 13.1074 11.5332 12.6934L11.2871 12.1279C10.8476 11.1193 10.0552 10.3163 9.06738 9.87695L8.30762 9.53906C7.89719 9.35628 7.89717 8.75795 8.30762 8.5752L9.02539 8.25684C10.0385 7.80623 10.8445 6.97345 11.2764 5.93066L11.5293 5.31934Z" fill="currentColor"/>`, "ai-agent": `<path d="M12 2C17.5228 2 22 6.47715 22 12C22 14.7096 20.9205 17.1697 19.1709 18.9697C17.3551 20.8376 14.8124 22 12 22C9.18756 22 6.64488 20.8376 4.8291 18.9697C3.07949 17.1697 2 14.7096 2 12C2 6.47715 6.47715 2 12 2ZM12 16C10.0022 16 8.20124 16.8375 6.9248 18.1816C8.30642 19.3175 10.0724 20 12 20C13.9274 20 15.6927 19.3173 17.0742 18.1816C15.7978 16.8377 13.9975 16 12 16ZM12 4C7.58172 4 4 7.58172 4 12C4 13.7701 4.57462 15.4044 5.54785 16.7295C7.1822 15.0483 9.46797 14 12 14C14.5318 14 16.8169 15.0485 18.4512 16.7295C19.4246 15.4043 20 13.7703 20 12C20 7.58172 16.4183 4 12 4ZM11.5293 5.31934C11.7058 4.89329 12.2943 4.89329 12.4707 5.31934L12.7236 5.93066C13.1556 6.97343 13.9615 7.80622 14.9746 8.25684L15.6924 8.5752C16.1029 8.75796 16.1028 9.35627 15.6924 9.53906L14.9326 9.87695C13.9448 10.3163 13.1534 11.1193 12.7139 12.1279L12.4668 12.6934C12.2864 13.1074 11.7137 13.1074 11.5332 12.6934L11.2871 12.1279C10.8476 11.1193 10.0552 10.3163 9.06738 9.87695L8.30762 9.53906C7.89719 9.35628 7.89717 8.75795 8.30762 8.5752L9.02539 8.25684C10.0385 7.80623 10.8445 6.97345 11.2764 5.93066L11.5293 5.31934Z" fill="currentColor"/>`,
"ai-agent-fill": `<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 15C9.71266 15 7.65042 15.961 6.19238 17.5C7.65042 19.039 9.71266 20 12 20C14.2871 20 16.3486 19.0387 17.8066 17.5C16.3486 15.9613 14.2871 15 12 15ZM12.4707 5.31934C12.2943 4.89337 11.7058 4.89339 11.5293 5.31934L11.2764 5.93066C10.8445 6.97341 10.0384 7.80621 9.02539 8.25684L8.30762 8.57617C7.89751 8.75905 7.89744 9.35625 8.30762 9.53906L9.06738 9.87695C10.0551 10.3163 10.8476 11.1193 11.2871 12.1279L11.5332 12.6934C11.7138 13.1073 12.2863 13.1073 12.4668 12.6934L12.7139 12.1279C13.1534 11.1194 13.9449 10.3163 14.9326 9.87695L15.6924 9.53906C16.1026 9.35624 16.1025 8.75907 15.6924 8.57617L14.9746 8.25684C13.9616 7.8062 13.1556 6.9734 12.7236 5.93066L12.4707 5.31934Z" fill="currentColor"/>`,
"ai-generate-2": `<path d="M20.4668 8.69379L20.7134 8.12811C21.1529 7.11947 21.9445 6.31641 22.9323 5.87708L23.6919 5.53922C24.1027 5.35653 24.1027 4.75881 23.6919 4.57612L22.9748 4.25714C21.9616 3.80651 21.1558 2.97373 20.7238 1.93083L20.4706 1.31953C20.2942 0.893489 19.7058 0.893489 19.5293 1.31953L19.2761 1.93083C18.8442 2.97373 18.0384 3.80651 17.0252 4.25714L16.308 4.57612C15.8973 4.75881 15.8973 5.35653 16.308 5.53922L17.0677 5.87708C18.0555 6.31641 18.8471 7.11947 19.2866 8.12811L19.5331 8.69379C19.7136 9.10792 20.2864 9.10792 20.4668 8.69379ZM5.79993 16H7.95399L8.55399 14.5H11.4459L12.0459 16H14.1999L10.9999 8H8.99993L5.79993 16ZM9.99993 10.8852L10.6459 12.5H9.35399L9.99993 10.8852ZM15 16V8H17V16H15ZM3 3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V11H20V19H4V5H14V3H3Z" fill="currentColor"/>`, "ai-generate-2": `<path d="M20.4668 8.69379L20.7134 8.12811C21.1529 7.11947 21.9445 6.31641 22.9323 5.87708L23.6919 5.53922C24.1027 5.35653 24.1027 4.75881 23.6919 4.57612L22.9748 4.25714C21.9616 3.80651 21.1558 2.97373 20.7238 1.93083L20.4706 1.31953C20.2942 0.893489 19.7058 0.893489 19.5293 1.31953L19.2761 1.93083C18.8442 2.97373 18.0384 3.80651 17.0252 4.25714L16.308 4.57612C15.8973 4.75881 15.8973 5.35653 16.308 5.53922L17.0677 5.87708C18.0555 6.31641 18.8471 7.11947 19.2866 8.12811L19.5331 8.69379C19.7136 9.10792 20.2864 9.10792 20.4668 8.69379ZM5.79993 16H7.95399L8.55399 14.5H11.4459L12.0459 16H14.1999L10.9999 8H8.99993L5.79993 16ZM9.99993 10.8852L10.6459 12.5H9.35399L9.99993 10.8852ZM15 16V8H17V16H15ZM3 3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V11H20V19H4V5H14V3H3Z" fill="currentColor"/>`,
"alert": `<path d="M12.8659 3.00017L22.3922 19.5002C22.6684 19.9785 22.5045 20.5901 22.0262 20.8662C21.8742 20.954 21.7017 21.0002 21.5262 21.0002H2.47363C1.92135 21.0002 1.47363 20.5525 1.47363 20.0002C1.47363 19.8246 1.51984 19.6522 1.60761 19.5002L11.1339 3.00017C11.41 2.52187 12.0216 2.358 12.4999 2.63414C12.6519 2.72191 12.7782 2.84815 12.8659 3.00017ZM4.20568 19.0002H19.7941L11.9999 5.50017L4.20568 19.0002ZM10.9999 16.0002H12.9999V18.0002H10.9999V16.0002ZM10.9999 9.00017H12.9999V14.0002H10.9999V9.00017Z" fill="currentColor"/>`, "alert": `<path d="M12.8659 3.00017L22.3922 19.5002C22.6684 19.9785 22.5045 20.5901 22.0262 20.8662C21.8742 20.954 21.7017 21.0002 21.5262 21.0002H2.47363C1.92135 21.0002 1.47363 20.5525 1.47363 20.0002C1.47363 19.8246 1.51984 19.6522 1.60761 19.5002L11.1339 3.00017C11.41 2.52187 12.0216 2.358 12.4999 2.63414C12.6519 2.72191 12.7782 2.84815 12.8659 3.00017ZM4.20568 19.0002H19.7941L11.9999 5.50017L4.20568 19.0002ZM10.9999 16.0002H12.9999V18.0002H10.9999V16.0002ZM10.9999 9.00017H12.9999V14.0002H10.9999V9.00017Z" fill="currentColor"/>`,
"align-justify": `<path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM3 14H21V16H3V14ZM3 9H21V11H3V9Z" fill="currentColor"/>`, "align-justify": `<path d="M3 4H21V6H3V4ZM3 19H21V21H3V19ZM3 14H21V16H3V14ZM3 9H21V11H3V9Z" fill="currentColor"/>`,
@@ -23,8 +23,8 @@ export const iconSpriteData = {
"arrow-left-s": `<path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z" fill="currentColor"/>`, "arrow-left-s": `<path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z" fill="currentColor"/>`,
"arrow-right": `<path d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z" fill="currentColor"/>`, "arrow-right": `<path d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z" fill="currentColor"/>`,
"arrow-right-s": `<path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z" fill="currentColor"/>`, "arrow-right-s": `<path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z" fill="currentColor"/>`,
"arrow-up-double": `<path d="M12 4.83582L5.79291 11.0429L7.20712 12.4571L12 7.66424L16.7929 12.4571L18.2071 11.0429L12 4.83582ZM12 10.4857L5.79291 16.6928L7.20712 18.107L12 13.3141L16.7929 18.107L18.2071 16.6928L12 10.4857Z" fill="currentColor"/>`,
"arrow-up": `<path d="M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z" fill="currentColor"/>`, "arrow-up": `<path d="M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z" fill="currentColor"/>`,
"arrow-up-double": `<path d="M12 4.83582L5.79291 11.0429L7.20712 12.4571L12 7.66424L16.7929 12.4571L18.2071 11.0429L12 4.83582ZM12 10.4857L5.79291 16.6928L7.20712 18.107L12 13.3141L16.7929 18.107L18.2071 16.6928L12 10.4857Z" fill="currentColor"/>`,
"arrow-up-s": `<path d="M11.9999 10.8284L7.0502 15.7782L5.63599 14.364L11.9999 8L18.3639 14.364L16.9497 15.7782L11.9999 10.8284Z" fill="currentColor"/>`, "arrow-up-s": `<path d="M11.9999 10.8284L7.0502 15.7782L5.63599 14.364L11.9999 8L18.3639 14.364L16.9497 15.7782L11.9999 10.8284Z" fill="currentColor"/>`,
"attachment-2": `<path d="M14.8287 7.75737L9.1718 13.4142C8.78127 13.8047 8.78127 14.4379 9.1718 14.8284C9.56232 15.219 10.1955 15.219 10.586 14.8284L16.2429 9.17158C17.4144 8.00001 17.4144 6.10052 16.2429 4.92894C15.0713 3.75737 13.1718 3.75737 12.0002 4.92894L6.34337 10.5858C4.39075 12.5384 4.39075 15.7042 6.34337 17.6569C8.29599 19.6095 11.4618 19.6095 13.4144 17.6569L19.0713 12L20.4855 13.4142L14.8287 19.0711C12.095 21.8047 7.66283 21.8047 4.92916 19.0711C2.19549 16.3374 2.19549 11.9053 4.92916 9.17158L10.586 3.51473C12.5386 1.56211 15.7045 1.56211 17.6571 3.51473C19.6097 5.46735 19.6097 8.63317 17.6571 10.5858L12.0002 16.2427C10.8287 17.4142 8.92916 17.4142 7.75759 16.2427C6.58601 15.0711 6.58601 13.1716 7.75759 12L13.4144 6.34316L14.8287 7.75737Z" fill="currentColor"/>`, "attachment-2": `<path d="M14.8287 7.75737L9.1718 13.4142C8.78127 13.8047 8.78127 14.4379 9.1718 14.8284C9.56232 15.219 10.1955 15.219 10.586 14.8284L16.2429 9.17158C17.4144 8.00001 17.4144 6.10052 16.2429 4.92894C15.0713 3.75737 13.1718 3.75737 12.0002 4.92894L6.34337 10.5858C4.39075 12.5384 4.39075 15.7042 6.34337 17.6569C8.29599 19.6095 11.4618 19.6095 13.4144 17.6569L19.0713 12L20.4855 13.4142L14.8287 19.0711C12.095 21.8047 7.66283 21.8047 4.92916 19.0711C2.19549 16.3374 2.19549 11.9053 4.92916 9.17158L10.586 3.51473C12.5386 1.56211 15.7045 1.56211 17.6571 3.51473C19.6097 5.46735 19.6097 8.63317 17.6571 10.5858L12.0002 16.2427C10.8287 17.4142 8.92916 17.4142 7.75759 16.2427C6.58601 15.0711 6.58601 13.1716 7.75759 12L13.4144 6.34316L14.8287 7.75737Z" fill="currentColor"/>`,
"bar-chart-2": `<path d="M2 13H8V21H2V13ZM16 8H22V21H16V8ZM9 3H15V21H9V3ZM4 15V19H6V15H4ZM11 5V19H13V5H11ZM18 10V19H20V10H18Z" fill="currentColor"/>`, "bar-chart-2": `<path d="M2 13H8V21H2V13ZM16 8H22V21H16V8ZM9 3H15V21H9V3ZM4 15V19H6V15H4ZM11 5V19H13V5H11ZM18 10V19H20V10H18Z" fill="currentColor"/>`,
@@ -32,8 +32,8 @@ export const iconSpriteData = {
"book": `<path d="M3 18.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22H6.5C4.567 22 3 20.433 3 18.5ZM19 20V17H6.5C5.67157 17 5 17.6716 5 18.5C5 19.3284 5.67157 20 6.5 20H19ZM5 15.3368C5.45463 15.1208 5.9632 15 6.5 15H19V4H6C5.44772 4 5 4.44772 5 5V15.3368Z" fill="currentColor"/>`, "book": `<path d="M3 18.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22H6.5C4.567 22 3 20.433 3 18.5ZM19 20V17H6.5C5.67157 17 5 17.6716 5 18.5C5 19.3284 5.67157 20 6.5 20H19ZM5 15.3368C5.45463 15.1208 5.9632 15 6.5 15H19V4H6C5.44772 4 5 4.44772 5 5V15.3368Z" fill="currentColor"/>`,
"book-open": `<path d="M13 21V23H11V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H9C10.1947 3 11.2671 3.52375 12 4.35418C12.7329 3.52375 13.8053 3 15 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H13ZM20 19V5H15C13.8954 5 13 5.89543 13 7V19H20ZM11 19V7C11 5.89543 10.1046 5 9 5H4V19H11Z" fill="currentColor"/>`, "book-open": `<path d="M13 21V23H11V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H9C10.1947 3 11.2671 3.52375 12 4.35418C12.7329 3.52375 13.8053 3 15 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H13ZM20 19V5H15C13.8954 5 13 5.89543 13 7V19H20ZM11 19V7C11 5.89543 10.1046 5 9 5H4V19H11Z" fill="currentColor"/>`,
"booklet": `<path d="M20.0049 2C21.1068 2 22 2.89821 22 3.9908V20.0092C22 21.1087 21.1074 22 20.0049 22H4V18H2V16H4V13H2V11H4V8H2V6H4V2H20.0049ZM8 4H6V20H8V4ZM20 4H10V20H20V4Z" fill="currentColor"/>`, "booklet": `<path d="M20.0049 2C21.1068 2 22 2.89821 22 3.9908V20.0092C22 21.1087 21.1074 22 20.0049 22H4V18H2V16H4V13H2V11H4V8H2V6H4V2H20.0049ZM8 4H6V20H8V4ZM20 4H10V20H20V4Z" fill="currentColor"/>`,
"brain-ai-3": `<path d="M19.5 4.7832V7.6709L22 9.11426V14.8867L19.499 16.3311L19.5 19.2178L14.5 22.1045L12 20.6611L9.5 22.1045L4.5 19.2178V16.3311L2 14.8877L2.00098 9.11328L4.5 7.66992V4.78418L9.5 1.89746L11.999 3.34082L14.501 1.89648L19.5 4.7832ZM13 5.07227V7H11V5.07324L9.5 4.20703L6.49902 5.93848V8.8252L4 10.2676V13.7334L6.5 15.1768V18.0635L9.5 19.7959L11 18.9287V17H13V18.9297L14.5 19.7959L17.5 18.0625V15.1768L20 13.7324V10.2695L17.499 8.8252L17.5 5.9375L14.501 4.20605L13 5.07227ZM14.2646 13.1602C14.3529 12.9473 14.6472 12.9473 14.7354 13.1602L14.8623 13.4648C15.0783 13.986 15.4807 14.4027 15.9873 14.6279L16.3457 14.7871C16.5511 14.8784 16.5511 15.1773 16.3457 15.2686L15.9658 15.4375C15.4721 15.6571 15.0761 16.0586 14.8564 16.5625L14.7334 16.8447C14.6432 17.0517 14.3569 17.0517 14.2666 16.8447L14.1436 16.5625C13.9239 16.0586 13.5279 15.6571 13.0342 15.4375L12.6543 15.2686C12.4489 15.1773 12.4489 14.8784 12.6543 14.7871L13.0127 14.6279C13.5193 14.4027 13.9217 13.986 14.1377 13.4648L14.2646 13.1602ZM9.58789 7.7793C9.74239 7.40671 10.2577 7.4067 10.4121 7.7793L10.6338 8.31445C11.0118 9.22695 11.7161 9.95624 12.6025 10.3506L13.2305 10.6289C13.5899 10.7887 13.5897 11.3117 13.2305 11.4717L12.5654 11.7676C11.7013 12.152 11.0086 12.8548 10.624 13.7373L10.4082 14.2324C10.2504 14.5948 9.74973 14.5948 9.5918 14.2324L9.37598 13.7373C8.99143 12.8548 8.29875 12.152 7.43457 11.7676L6.76953 11.4717C6.41033 11.3117 6.41022 10.7887 6.76953 10.6289L7.39746 10.3506C8.2839 9.95624 8.98832 9.22697 9.36621 8.31445L9.58789 7.7793Z" fill="currentColor"/>`,
"brain": `<path d="M9 4C10.1046 4 11 4.89543 11 6V12.8271C10.1058 12.1373 8.96602 11.7305 7.6644 11.5136L7.3356 13.4864C8.71622 13.7165 9.59743 14.1528 10.1402 14.7408C10.67 15.3147 11 16.167 11 17.5C11 18.8807 9.88071 20 8.5 20C7.11929 20 6 18.8807 6 17.5V17.1493C6.43007 17.2926 6.87634 17.4099 7.3356 17.4864L7.6644 15.5136C6.92149 15.3898 6.1752 15.1144 5.42909 14.7599C4.58157 14.3573 4 13.499 4 12.5C4 11.6653 4.20761 11.0085 4.55874 10.5257C4.90441 10.0504 5.4419 9.6703 6.24254 9.47014L7 9.28078V6C7 4.89543 7.89543 4 9 4ZM12 3.35418C11.2671 2.52376 10.1947 2 9 2C6.79086 2 5 3.79086 5 6V7.77422C4.14895 8.11644 3.45143 8.64785 2.94126 9.34933C2.29239 10.2415 2 11.3347 2 12.5C2 14.0652 2.79565 15.4367 4 16.2422V17.5C4 19.9853 6.01472 22 8.5 22C9.91363 22 11.175 21.3482 12 20.3287C12.825 21.3482 14.0864 22 15.5 22C17.9853 22 20 19.9853 20 17.5V16.2422C21.2044 15.4367 22 14.0652 22 12.5C22 11.3347 21.7076 10.2415 21.0587 9.34933C20.5486 8.64785 19.8511 8.11644 19 7.77422V6C19 3.79086 17.2091 2 15 2C13.8053 2 12.7329 2.52376 12 3.35418ZM18 17.1493V17.5C18 18.8807 16.8807 20 15.5 20C14.1193 20 13 18.8807 13 17.5C13 16.167 13.33 15.3147 13.8598 14.7408C14.4026 14.1528 15.2838 13.7165 16.6644 13.4864L16.3356 11.5136C15.034 11.7305 13.8942 12.1373 13 12.8271V6C13 4.89543 13.8954 4 15 4C16.1046 4 17 4.89543 17 6V9.28078L17.7575 9.47014C18.5581 9.6703 19.0956 10.0504 19.4413 10.5257C19.7924 11.0085 20 11.6653 20 12.5C20 13.499 19.4184 14.3573 18.5709 14.7599C17.8248 15.1144 17.0785 15.3898 16.3356 15.5136L16.6644 17.4864C17.1237 17.4099 17.5699 17.2926 18 17.1493Z" fill="currentColor"/>`, "brain": `<path d="M9 4C10.1046 4 11 4.89543 11 6V12.8271C10.1058 12.1373 8.96602 11.7305 7.6644 11.5136L7.3356 13.4864C8.71622 13.7165 9.59743 14.1528 10.1402 14.7408C10.67 15.3147 11 16.167 11 17.5C11 18.8807 9.88071 20 8.5 20C7.11929 20 6 18.8807 6 17.5V17.1493C6.43007 17.2926 6.87634 17.4099 7.3356 17.4864L7.6644 15.5136C6.92149 15.3898 6.1752 15.1144 5.42909 14.7599C4.58157 14.3573 4 13.499 4 12.5C4 11.6653 4.20761 11.0085 4.55874 10.5257C4.90441 10.0504 5.4419 9.6703 6.24254 9.47014L7 9.28078V6C7 4.89543 7.89543 4 9 4ZM12 3.35418C11.2671 2.52376 10.1947 2 9 2C6.79086 2 5 3.79086 5 6V7.77422C4.14895 8.11644 3.45143 8.64785 2.94126 9.34933C2.29239 10.2415 2 11.3347 2 12.5C2 14.0652 2.79565 15.4367 4 16.2422V17.5C4 19.9853 6.01472 22 8.5 22C9.91363 22 11.175 21.3482 12 20.3287C12.825 21.3482 14.0864 22 15.5 22C17.9853 22 20 19.9853 20 17.5V16.2422C21.2044 15.4367 22 14.0652 22 12.5C22 11.3347 21.7076 10.2415 21.0587 9.34933C20.5486 8.64785 19.8511 8.11644 19 7.77422V6C19 3.79086 17.2091 2 15 2C13.8053 2 12.7329 2.52376 12 3.35418ZM18 17.1493V17.5C18 18.8807 16.8807 20 15.5 20C14.1193 20 13 18.8807 13 17.5C13 16.167 13.33 15.3147 13.8598 14.7408C14.4026 14.1528 15.2838 13.7165 16.6644 13.4864L16.3356 11.5136C15.034 11.7305 13.8942 12.1373 13 12.8271V6C13 4.89543 13.8954 4 15 4C16.1046 4 17 4.89543 17 6V9.28078L17.7575 9.47014C18.5581 9.6703 19.0956 10.0504 19.4413 10.5257C19.7924 11.0085 20 11.6653 20 12.5C20 13.499 19.4184 14.3573 18.5709 14.7599C17.8248 15.1144 17.0785 15.3898 16.3356 15.5136L16.6644 17.4864C17.1237 17.4099 17.5699 17.2926 18 17.1493Z" fill="currentColor"/>`,
"brain-ai-3": `<path d="M19.5 4.7832V7.6709L22 9.11426V14.8867L19.499 16.3311L19.5 19.2178L14.5 22.1045L12 20.6611L9.5 22.1045L4.5 19.2178V16.3311L2 14.8877L2.00098 9.11328L4.5 7.66992V4.78418L9.5 1.89746L11.999 3.34082L14.501 1.89648L19.5 4.7832ZM13 5.07227V7H11V5.07324L9.5 4.20703L6.49902 5.93848V8.8252L4 10.2676V13.7334L6.5 15.1768V18.0635L9.5 19.7959L11 18.9287V17H13V18.9297L14.5 19.7959L17.5 18.0625V15.1768L20 13.7324V10.2695L17.499 8.8252L17.5 5.9375L14.501 4.20605L13 5.07227ZM14.2646 13.1602C14.3529 12.9473 14.6472 12.9473 14.7354 13.1602L14.8623 13.4648C15.0783 13.986 15.4807 14.4027 15.9873 14.6279L16.3457 14.7871C16.5511 14.8784 16.5511 15.1773 16.3457 15.2686L15.9658 15.4375C15.4721 15.6571 15.0761 16.0586 14.8564 16.5625L14.7334 16.8447C14.6432 17.0517 14.3569 17.0517 14.2666 16.8447L14.1436 16.5625C13.9239 16.0586 13.5279 15.6571 13.0342 15.4375L12.6543 15.2686C12.4489 15.1773 12.4489 14.8784 12.6543 14.7871L13.0127 14.6279C13.5193 14.4027 13.9217 13.986 14.1377 13.4648L14.2646 13.1602ZM9.58789 7.7793C9.74239 7.40671 10.2577 7.4067 10.4121 7.7793L10.6338 8.31445C11.0118 9.22695 11.7161 9.95624 12.6025 10.3506L13.2305 10.6289C13.5899 10.7887 13.5897 11.3117 13.2305 11.4717L12.5654 11.7676C11.7013 12.152 11.0086 12.8548 10.624 13.7373L10.4082 14.2324C10.2504 14.5948 9.74973 14.5948 9.5918 14.2324L9.37598 13.7373C8.99143 12.8548 8.29875 12.152 7.43457 11.7676L6.76953 11.4717C6.41033 11.3117 6.41022 10.7887 6.76953 10.6289L7.39746 10.3506C8.2839 9.95624 8.98832 9.22697 9.36621 8.31445L9.58789 7.7793Z" fill="currentColor"/>`,
"briefcase": `<path d="M7 5V2C7 1.44772 7.44772 1 8 1H16C16.5523 1 17 1.44772 17 2V5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V6C2 5.44772 2.44772 5 3 5H7ZM4 16V19H20V16H4ZM4 14H20V7H4V14ZM9 3V5H15V3H9ZM11 11H13V13H11V11Z" fill="currentColor"/>`, "briefcase": `<path d="M7 5V2C7 1.44772 7.44772 1 8 1H16C16.5523 1 17 1.44772 17 2V5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V6C2 5.44772 2.44772 5 3 5H7ZM4 16V19H20V16H4ZM4 14H20V7H4V14ZM9 3V5H15V3H9ZM11 11H13V13H11V11Z" fill="currentColor"/>`,
"bug": `<path d="M13 19.9C15.2822 19.4367 17 17.419 17 15V12C17 11.299 16.8564 10.6219 16.5846 10H7.41538C7.14358 10.6219 7 11.299 7 12V15C7 17.419 8.71776 19.4367 11 19.9V14H13V19.9ZM5.5358 17.6907C5.19061 16.8623 5 15.9534 5 15H2V13H5V12C5 11.3573 5.08661 10.7348 5.2488 10.1436L3.0359 8.86602L4.0359 7.13397L6.05636 8.30049C6.11995 8.19854 6.18609 8.09835 6.25469 8H17.7453C17.8139 8.09835 17.88 8.19854 17.9436 8.30049L19.9641 7.13397L20.9641 8.86602L18.7512 10.1436C18.9134 10.7348 19 11.3573 19 12V13H22V15H19C19 15.9534 18.8094 16.8623 18.4642 17.6907L20.9641 19.134L19.9641 20.866L17.4383 19.4077C16.1549 20.9893 14.1955 22 12 22C9.80453 22 7.84512 20.9893 6.56171 19.4077L4.0359 20.866L3.0359 19.134L5.5358 17.6907ZM8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6H8Z" fill="currentColor"/>`, "bug": `<path d="M13 19.9C15.2822 19.4367 17 17.419 17 15V12C17 11.299 16.8564 10.6219 16.5846 10H7.41538C7.14358 10.6219 7 11.299 7 12V15C7 17.419 8.71776 19.4367 11 19.9V14H13V19.9ZM5.5358 17.6907C5.19061 16.8623 5 15.9534 5 15H2V13H5V12C5 11.3573 5.08661 10.7348 5.2488 10.1436L3.0359 8.86602L4.0359 7.13397L6.05636 8.30049C6.11995 8.19854 6.18609 8.09835 6.25469 8H17.7453C17.8139 8.09835 17.88 8.19854 17.9436 8.30049L19.9641 7.13397L20.9641 8.86602L18.7512 10.1436C18.9134 10.7348 19 11.3573 19 12V13H22V15H19C19 15.9534 18.8094 16.8623 18.4642 17.6907L20.9641 19.134L19.9641 20.866L17.4383 19.4077C16.1549 20.9893 14.1955 22 12 22C9.80453 22 7.84512 20.9893 6.56171 19.4077L4.0359 20.866L3.0359 19.134L5.5358 17.6907ZM8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6H8Z" fill="currentColor"/>`,
"calendar": `<path d="M9 1V3H15V1H17V3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H7V1H9ZM20 11H4V19H20V11ZM7 5H4V9H20V5H17V7H15V5H9V7H7V5Z" fill="currentColor"/>`, "calendar": `<path d="M9 1V3H15V1H17V3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H7V1H9ZM20 11H4V19H20V11ZM7 5H4V9H20V5H17V7H15V5H9V7H7V5Z" fill="currentColor"/>`,
@@ -47,19 +47,19 @@ export const iconSpriteData = {
"chat-new": `<path d="M14 3V5H4V18.3851L5.76282 17H20V10H22V18C22 18.5523 21.5523 19 21 19H6.45455L2 22.5V4C2 3.44772 2.44772 3 3 3H14ZM19 3V0H21V3H24V5H21V8H19V5H16V3H19Z" fill="currentColor"/>`, "chat-new": `<path d="M14 3V5H4V18.3851L5.76282 17H20V10H22V18C22 18.5523 21.5523 19 21 19H6.45455L2 22.5V4C2 3.44772 2.44772 3 3 3H14ZM19 3V0H21V3H24V5H21V8H19V5H16V3H19Z" fill="currentColor"/>`,
"chat-thread": `<path d="M2 22L7.29117 20.8242C8.6944 21.5746 10.2975 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 13.7025 2.42544 15.3056 3.17581 16.7088L2 22ZM8.23428 19.0605L7.58075 18.711L4.63416 19.3658L5.28896 16.4192L4.93949 15.7657C4.32549 14.6175 4 13.3345 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C10.6655 20 9.38248 19.6745 8.23428 19.0605ZM15.4487 7H13.4411L13.2661 9.00024H11.2584L11.4334 7H9.42577L9.25077 9.00024H7V11.0002H9.0758L8.90082 13.0002H7V15.0002H8.72584L8.55089 17H10.5585L10.7335 15.0002H12.7411L12.5662 17H14.5738L14.7488 15.0002H17V13.0002H14.9237L15.0987 11.0002H17V9.00024H15.2737L15.4487 7ZM11.0834 11.0002H13.0911L12.9161 13.0002H10.9085L11.0834 11.0002Z" fill="currentColor"/>`, "chat-thread": `<path d="M2 22L7.29117 20.8242C8.6944 21.5746 10.2975 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 13.7025 2.42544 15.3056 3.17581 16.7088L2 22ZM8.23428 19.0605L7.58075 18.711L4.63416 19.3658L5.28896 16.4192L4.93949 15.7657C4.32549 14.6175 4 13.3345 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C10.6655 20 9.38248 19.6745 8.23428 19.0605ZM15.4487 7H13.4411L13.2661 9.00024H11.2584L11.4334 7H9.42577L9.25077 9.00024H7V11.0002H9.0758L8.90082 13.0002H7V15.0002H8.72584L8.55089 17H10.5585L10.7335 15.0002H12.7411L12.5662 17H14.5738L14.7488 15.0002H17V13.0002H14.9237L15.0987 11.0002H17V9.00024H15.2737L15.4487 7ZM11.0834 11.0002H13.0911L12.9161 13.0002H10.9085L11.0834 11.0002Z" fill="currentColor"/>`,
"check": `<path d="M9.9997 15.1709L19.1921 5.97852L20.6063 7.39273L9.9997 17.9993L3.63574 11.6354L5.04996 10.2212L9.9997 15.1709Z" fill="currentColor"/>`, "check": `<path d="M9.9997 15.1709L19.1921 5.97852L20.6063 7.39273L9.9997 17.9993L3.63574 11.6354L5.04996 10.2212L9.9997 15.1709Z" fill="currentColor"/>`,
"checkbox-blank-circle-fill": `<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor"/>`,
"checkbox-blank": `<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5Z" fill="currentColor"/>`,
"checkbox-circle": `<path d="M4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM17.4571 9.45711L16.0429 8.04289L11 13.0858L8.20711 10.2929L6.79289 11.7071L11 15.9142L17.4571 9.45711Z" fill="currentColor"/>`,
"checkbox": `<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5ZM11.0026 16L6.75999 11.7574L8.17421 10.3431L11.0026 13.1716L16.6595 7.51472L18.0737 8.92893L11.0026 16Z" fill="currentColor"/>`, "checkbox": `<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5ZM11.0026 16L6.75999 11.7574L8.17421 10.3431L11.0026 13.1716L16.6595 7.51472L18.0737 8.92893L11.0026 16Z" fill="currentColor"/>`,
"checkbox-blank": `<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5Z" fill="currentColor"/>`,
"checkbox-blank-circle-fill": `<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor"/>`,
"checkbox-circle": `<path d="M4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM17.4571 9.45711L16.0429 8.04289L11 13.0858L8.20711 10.2929L6.79289 11.7071L11 15.9142L17.4571 9.45711Z" fill="currentColor"/>`,
"checkbox-multiple": `<path d="M6.99979 7V3C6.99979 2.44772 7.4475 2 7.99979 2H20.9998C21.5521 2 21.9998 2.44772 21.9998 3V16C21.9998 16.5523 21.5521 17 20.9998 17H17V20.9925C17 21.5489 16.551 22 15.9925 22H3.00728C2.45086 22 2 21.5511 2 20.9925L2.00276 8.00748C2.00288 7.45107 2.4518 7 3.01025 7H6.99979ZM8.99979 7H15.9927C16.549 7 17 7.44892 17 8.00748V15H19.9998V4H8.99979V7ZM15 9H4.00255L4.00021 20H15V9ZM8.50242 18L4.96689 14.4645L6.3811 13.0503L8.50242 15.1716L12.7451 10.9289L14.1593 12.3431L8.50242 18Z" fill="currentColor"/>`, "checkbox-multiple": `<path d="M6.99979 7V3C6.99979 2.44772 7.4475 2 7.99979 2H20.9998C21.5521 2 21.9998 2.44772 21.9998 3V16C21.9998 16.5523 21.5521 17 20.9998 17H17V20.9925C17 21.5489 16.551 22 15.9925 22H3.00728C2.45086 22 2 21.5511 2 20.9925L2.00276 8.00748C2.00288 7.45107 2.4518 7 3.01025 7H6.99979ZM8.99979 7H15.9927C16.549 7 17 7.44892 17 8.00748V15H19.9998V4H8.99979V7ZM15 9H4.00255L4.00021 20H15V9ZM8.50242 18L4.96689 14.4645L6.3811 13.0503L8.50242 15.1716L12.7451 10.9289L14.1593 12.3431L8.50242 18Z" fill="currentColor"/>`,
"clipboard": `<path d="M7 4V2H17V4H20.0066C20.5552 4 21 4.44495 21 4.9934V21.0066C21 21.5552 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5551 3 21.0066V4.9934C3 4.44476 3.44495 4 3.9934 4H7ZM7 6H5V20H19V6H17V8H7V6ZM9 4V6H15V4H9Z" fill="currentColor"/>`, "clipboard": `<path d="M7 4V2H17V4H20.0066C20.5552 4 21 4.44495 21 4.9934V21.0066C21 21.5552 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5551 3 21.0066V4.9934C3 4.44476 3.44495 4 3.9934 4H7ZM7 6H5V20H19V6H17V8H7V6ZM9 4V6H15V4H9Z" fill="currentColor"/>`,
"close-circle": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 10.5858L14.8284 7.75736L16.2426 9.17157L13.4142 12L16.2426 14.8284L14.8284 16.2426L12 13.4142L9.17157 16.2426L7.75736 14.8284L10.5858 12L7.75736 9.17157L9.17157 7.75736L12 10.5858Z" fill="currentColor"/>`,
"close": `<path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z" fill="currentColor"/>`, "close": `<path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z" fill="currentColor"/>`,
"close-circle": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 10.5858L14.8284 7.75736L16.2426 9.17157L13.4142 12L16.2426 14.8284L14.8284 16.2426L12 13.4142L9.17157 16.2426L7.75736 14.8284L10.5858 12L7.75736 9.17157L9.17157 7.75736L12 10.5858Z" fill="currentColor"/>`,
"cloud": `<path d="M12 2C15.866 2 19 5.13401 19 9C19 9.11351 18.9973 9.22639 18.992 9.33857C21.3265 10.16 23 12.3846 23 15C23 18.3137 20.3137 21 17 21H7C3.68629 21 1 18.3137 1 15C1 12.3846 2.67346 10.16 5.00804 9.33857C5.0027 9.22639 5 9.11351 5 9C5 5.13401 8.13401 2 12 2ZM12 4C9.23858 4 7 6.23858 7 9C7 9.08147 7.00193 9.16263 7.00578 9.24344L7.07662 10.7309L5.67183 11.2252C4.0844 11.7837 3 13.2889 3 15C3 17.2091 4.79086 19 7 19H17C19.2091 19 21 17.2091 21 15C21 12.79 19.21 11 17 11C15.233 11 13.7337 12.1457 13.2042 13.7347L11.3064 13.1021C12.1005 10.7185 14.35 9 17 9C17 6.23858 14.7614 4 12 4Z" fill="currentColor"/>`, "cloud": `<path d="M12 2C15.866 2 19 5.13401 19 9C19 9.11351 18.9973 9.22639 18.992 9.33857C21.3265 10.16 23 12.3846 23 15C23 18.3137 20.3137 21 17 21H7C3.68629 21 1 18.3137 1 15C1 12.3846 2.67346 10.16 5.00804 9.33857C5.0027 9.22639 5 9.11351 5 9C5 5.13401 8.13401 2 12 2ZM12 4C9.23858 4 7 6.23858 7 9C7 9.08147 7.00193 9.16263 7.00578 9.24344L7.07662 10.7309L5.67183 11.2252C4.0844 11.7837 3 13.2889 3 15C3 17.2091 4.79086 19 7 19H17C19.2091 19 21 17.2091 21 15C21 12.79 19.21 11 17 11C15.233 11 13.7337 12.1457 13.2042 13.7347L11.3064 13.1021C12.1005 10.7185 14.35 9 17 9C17 6.23858 14.7614 4 12 4Z" fill="currentColor"/>`,
"cloud-off": `<path d="M3.51472 2.10051L22.6066 21.1924L21.1924 22.6066L19.1782 20.5924C18.503 20.8556 17.7684 21 17 21H7C3.68629 21 1 18.3137 1 15C1 12.3846 2.67346 10.16 5.00804 9.33857C5.0027 9.22639 5 9.11351 5 9C5 8.22228 5.12683 7.47418 5.36094 6.77527L2.10051 3.51472L3.51472 2.10051ZM7 9C7 9.08147 7.00193 9.16263 7.00578 9.24344L7.07662 10.7309L5.67183 11.2252C4.0844 11.7837 3 13.2889 3 15C3 17.2091 4.79086 19 7 19H17C17.1858 19 17.3687 18.9873 17.5478 18.9628L7.03043 8.44519C7.01032 8.62736 7 8.81247 7 9ZM12 2C15.866 2 19 5.13401 19 9C19 9.11351 18.9973 9.22639 18.992 9.33857C21.3265 10.16 23 12.3846 23 15C23 16.0883 22.7103 17.1089 22.2037 17.9889L20.7111 16.4955C20.8974 16.0335 21 15.5287 21 15C21 12.79 19.21 11 17 11C16.4711 11 15.9661 11.1027 15.5039 11.2892L14.0111 9.7964C14.8912 9.28978 15.9118 9 17 9C17 6.23858 14.7614 4 12 4C10.9295 4 9.93766 4.33639 9.12428 4.90922L7.69418 3.48056C8.88169 2.55284 10.3763 2 12 2Z" fill="currentColor"/>`, "cloud-off": `<path d="M3.51472 2.10051L22.6066 21.1924L21.1924 22.6066L19.1782 20.5924C18.503 20.8556 17.7684 21 17 21H7C3.68629 21 1 18.3137 1 15C1 12.3846 2.67346 10.16 5.00804 9.33857C5.0027 9.22639 5 9.11351 5 9C5 8.22228 5.12683 7.47418 5.36094 6.77527L2.10051 3.51472L3.51472 2.10051ZM7 9C7 9.08147 7.00193 9.16263 7.00578 9.24344L7.07662 10.7309L5.67183 11.2252C4.0844 11.7837 3 13.2889 3 15C3 17.2091 4.79086 19 7 19H17C17.1858 19 17.3687 18.9873 17.5478 18.9628L7.03043 8.44519C7.01032 8.62736 7 8.81247 7 9ZM12 2C15.866 2 19 5.13401 19 9C19 9.11351 18.9973 9.22639 18.992 9.33857C21.3265 10.16 23 12.3846 23 15C23 16.0883 22.7103 17.1089 22.2037 17.9889L20.7111 16.4955C20.8974 16.0335 21 15.5287 21 15C21 12.79 19.21 11 17 11C16.4711 11 15.9661 11.1027 15.5039 11.2892L14.0111 9.7964C14.8912 9.28978 15.9118 9 17 9C17 6.23858 14.7614 4 12 4C10.9295 4 9.93766 4.33639 9.12428 4.90922L7.69418 3.48056C8.88169 2.55284 10.3763 2 12 2Z" fill="currentColor"/>`,
"code": `<path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z" fill="currentColor"/>`,
"code-ai": `<path d="M17.7134 10.1281L17.4668 10.6938C17.2864 11.1079 16.7136 11.1079 16.5331 10.6938L16.2866 10.1281C15.8471 9.11947 15.0555 8.31641 14.0677 7.87708L13.308 7.53922C12.8973 7.35653 12.8973 6.75881 13.308 6.57612L14.0252 6.25714C15.0384 5.80651 15.8442 4.97373 16.2761 3.93083L16.5293 3.31953C16.7058 2.89349 17.2942 2.89349 17.4706 3.31953L17.7238 3.93083C18.1558 4.97373 18.9616 5.80651 19.9748 6.25714L20.6919 6.57612C21.1027 6.75881 21.1027 7.35653 20.6919 7.53922L19.9323 7.87708C18.9445 8.31641 18.1529 9.11947 17.7134 10.1281ZM2.82843 12.0001L7.07107 16.2428L5.65685 17.657L0 12.0001L5.65685 6.34326L7.07107 7.75748L2.82843 12.0001ZM18.3429 17.6572L23.9998 12.0003L21.1714 9.17188L19.7571 10.5861L21.1714 12.0003L16.9287 16.2429L18.3429 17.6572Z" fill="currentColor"/>`, "code-ai": `<path d="M17.7134 10.1281L17.4668 10.6938C17.2864 11.1079 16.7136 11.1079 16.5331 10.6938L16.2866 10.1281C15.8471 9.11947 15.0555 8.31641 14.0677 7.87708L13.308 7.53922C12.8973 7.35653 12.8973 6.75881 13.308 6.57612L14.0252 6.25714C15.0384 5.80651 15.8442 4.97373 16.2761 3.93083L16.5293 3.31953C16.7058 2.89349 17.2942 2.89349 17.4706 3.31953L17.7238 3.93083C18.1558 4.97373 18.9616 5.80651 19.9748 6.25714L20.6919 6.57612C21.1027 6.75881 21.1027 7.35653 20.6919 7.53922L19.9323 7.87708C18.9445 8.31641 18.1529 9.11947 17.7134 10.1281ZM2.82843 12.0001L7.07107 16.2428L5.65685 17.657L0 12.0001L5.65685 6.34326L7.07107 7.75748L2.82843 12.0001ZM18.3429 17.6572L23.9998 12.0003L21.1714 9.17188L19.7571 10.5861L21.1714 12.0003L16.9287 16.2429L18.3429 17.6572Z" fill="currentColor"/>`,
"code-box": `<path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM4 5V19H20V5H4ZM20 12L16.4645 15.5355L15.0503 14.1213L17.1716 12L15.0503 9.87868L16.4645 8.46447L20 12ZM6.82843 12L8.94975 14.1213L7.53553 15.5355L4 12L7.53553 8.46447L8.94975 9.87868L6.82843 12ZM11.2443 17H9.11597L12.7557 7H14.884L11.2443 17Z" fill="currentColor"/>`, "code-box": `<path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM4 5V19H20V5H4ZM20 12L16.4645 15.5355L15.0503 14.1213L17.1716 12L15.0503 9.87868L16.4645 8.46447L20 12ZM6.82843 12L8.94975 14.1213L7.53553 15.5355L4 12L7.53553 8.46447L8.94975 9.87868L6.82843 12ZM11.2443 17H9.11597L12.7557 7H14.884L11.2443 17Z" fill="currentColor"/>`,
"code": `<path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z" fill="currentColor"/>`,
"code-sslash": `<path d="M24 12L18.3431 17.6569L16.9289 16.2426L21.1716 12L16.9289 7.75736L18.3431 6.34315L24 12ZM2.82843 12L7.07107 16.2426L5.65685 17.6569L0 12L5.65685 6.34315L7.07107 7.75736L2.82843 12ZM9.78845 21H7.66009L14.2116 3H16.3399L9.78845 21Z" fill="currentColor"/>`, "code-sslash": `<path d="M24 12L18.3431 17.6569L16.9289 16.2426L21.1716 12L16.9289 7.75736L18.3431 6.34315L24 12ZM2.82843 12L7.07107 16.2426L5.65685 17.6569L0 12L5.65685 6.34315L7.07107 7.75736L2.82843 12ZM9.78845 21H7.66009L14.2116 3H16.3399L9.78845 21Z" fill="currentColor"/>`,
"command": `<path d="M10 8H14V6.5C14 4.567 15.567 3 17.5 3C19.433 3 21 4.567 21 6.5C21 8.433 19.433 10 17.5 10H16V14H17.5C19.433 14 21 15.567 21 17.5C21 19.433 19.433 21 17.5 21C15.567 21 14 19.433 14 17.5V16H10V17.5C10 19.433 8.433 21 6.5 21C4.567 21 3 19.433 3 17.5C3 15.567 4.567 14 6.5 14H8V10H6.5C4.567 10 3 8.433 3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5V8ZM8 8V6.5C8 5.67157 7.32843 5 6.5 5C5.67157 5 5 5.67157 5 6.5C5 7.32843 5.67157 8 6.5 8H8ZM8 16H6.5C5.67157 16 5 16.6716 5 17.5C5 18.3284 5.67157 19 6.5 19C7.32843 19 8 18.3284 8 17.5V16ZM16 8H17.5C18.3284 8 19 7.32843 19 6.5C19 5.67157 18.3284 5 17.5 5C16.6716 5 16 5.67157 16 6.5V8ZM16 16V17.5C16 18.3284 16.6716 19 17.5 19C18.3284 19 19 18.3284 19 17.5C19 16.6716 18.3284 16 17.5 16H16ZM10 10V14H14V10H10Z" fill="currentColor"/>`, "command": `<path d="M10 8H14V6.5C14 4.567 15.567 3 17.5 3C19.433 3 21 4.567 21 6.5C21 8.433 19.433 10 17.5 10H16V14H17.5C19.433 14 21 15.567 21 17.5C21 19.433 19.433 21 17.5 21C15.567 21 14 19.433 14 17.5V16H10V17.5C10 19.433 8.433 21 6.5 21C4.567 21 3 19.433 3 17.5C3 15.567 4.567 14 6.5 14H8V10H6.5C4.567 10 3 8.433 3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5V8ZM8 8V6.5C8 5.67157 7.32843 5 6.5 5C5.67157 5 5 5.67157 5 6.5C5 7.32843 5.67157 8 6.5 8H8ZM8 16H6.5C5.67157 16 5 16.6716 5 17.5C5 18.3284 5.67157 19 6.5 19C7.32843 19 8 18.3284 8 17.5V16ZM16 8H17.5C18.3284 8 19 7.32843 19 6.5C19 5.67157 18.3284 5 17.5 5C16.6716 5 16 5.67157 16 6.5V8ZM16 16V17.5C16 18.3284 16.6716 19 17.5 19C18.3284 19 19 18.3284 19 17.5C19 16.6716 18.3284 16 17.5 16H16ZM10 10V14H14V10H10Z" fill="currentColor"/>`,
"compass-3": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM16.5 7.5L14 14L7.5 16.5L10 10L16.5 7.5ZM12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill="currentColor"/>`, "compass-3": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM16.5 7.5L14 14L7.5 16.5L10 10L16.5 7.5ZM12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill="currentColor"/>`,
@@ -70,14 +70,14 @@ export const iconSpriteData = {
"database-2": `<path d="M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z" fill="currentColor"/>`, "database-2": `<path d="M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z" fill="currentColor"/>`,
"delete-bin": `<path d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z" fill="currentColor"/>`, "delete-bin": `<path d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z" fill="currentColor"/>`,
"discord-fill": `<path d="M19.3034 5.33716C17.9344 4.71103 16.4805 4.2547 14.9629 4C14.7719 4.32899 14.5596 4.77471 14.411 5.12492C12.7969 4.89144 11.1944 4.89144 9.60255 5.12492C9.45397 4.77471 9.2311 4.32899 9.05068 4C7.52251 4.2547 6.06861 4.71103 4.70915 5.33716C1.96053 9.39111 1.21766 13.3495 1.5891 17.2549C3.41443 18.5815 5.17612 19.388 6.90701 19.9187C7.33151 19.3456 7.71356 18.73 8.04255 18.0827C7.41641 17.8492 6.82211 17.5627 6.24904 17.2231C6.39762 17.117 6.5462 17.0003 6.68416 16.8835C10.1438 18.4648 13.8911 18.4648 17.3082 16.8835C17.4568 17.0003 17.5948 17.117 17.7434 17.2231C17.1703 17.5627 16.576 17.8492 15.9499 18.0827C16.2789 18.73 16.6609 19.3456 17.0854 19.9187C18.8152 19.388 20.5875 18.5815 22.4033 17.2549C22.8596 12.7341 21.6806 8.80747 19.3034 5.33716ZM8.5201 14.8459C7.48007 14.8459 6.63107 13.9014 6.63107 12.7447C6.63107 11.5879 7.45884 10.6434 8.5201 10.6434C9.57071 10.6434 10.4303 11.5879 10.4091 12.7447C10.4091 13.9014 9.57071 14.8459 8.5201 14.8459ZM15.4936 14.8459C14.4535 14.8459 13.6034 13.9014 13.6034 12.7447C13.6034 11.5879 14.4323 10.6434 15.4936 10.6434C16.5442 10.6434 17.4038 11.5879 17.3825 12.7447C17.3825 13.9014 16.5548 14.8459 15.4936 14.8459Z" fill="currentColor"/>`, "discord-fill": `<path d="M19.3034 5.33716C17.9344 4.71103 16.4805 4.2547 14.9629 4C14.7719 4.32899 14.5596 4.77471 14.411 5.12492C12.7969 4.89144 11.1944 4.89144 9.60255 5.12492C9.45397 4.77471 9.2311 4.32899 9.05068 4C7.52251 4.2547 6.06861 4.71103 4.70915 5.33716C1.96053 9.39111 1.21766 13.3495 1.5891 17.2549C3.41443 18.5815 5.17612 19.388 6.90701 19.9187C7.33151 19.3456 7.71356 18.73 8.04255 18.0827C7.41641 17.8492 6.82211 17.5627 6.24904 17.2231C6.39762 17.117 6.5462 17.0003 6.68416 16.8835C10.1438 18.4648 13.8911 18.4648 17.3082 16.8835C17.4568 17.0003 17.5948 17.117 17.7434 17.2231C17.1703 17.5627 16.576 17.8492 15.9499 18.0827C16.2789 18.73 16.6609 19.3456 17.0854 19.9187C18.8152 19.388 20.5875 18.5815 22.4033 17.2549C22.8596 12.7341 21.6806 8.80747 19.3034 5.33716ZM8.5201 14.8459C7.48007 14.8459 6.63107 13.9014 6.63107 12.7447C6.63107 11.5879 7.45884 10.6434 8.5201 10.6434C9.57071 10.6434 10.4303 11.5879 10.4091 12.7447C10.4091 13.9014 9.57071 14.8459 8.5201 14.8459ZM15.4936 14.8459C14.4535 14.8459 13.6034 13.9014 13.6034 12.7447C13.6034 11.5879 14.4323 10.6434 15.4936 10.6434C16.5442 10.6434 17.4038 11.5879 17.3825 12.7447C17.3825 13.9014 16.5548 14.8459 15.4936 14.8459Z" fill="currentColor"/>`,
"donut-chart-fill": `<path d="M10.9999 2.04938L11 5.07088C7.6077 5.55612 5 8.47352 5 12C5 15.866 8.13401 19 12 19C13.5723 19 15.0236 18.4816 16.1922 17.6064L18.3289 19.7428C16.605 21.1536 14.4014 22 12 22C6.47715 22 2 17.5228 2 12C2 6.81468 5.94662 2.55115 10.9999 2.04938ZM21.9506 13.0001C21.7509 15.0111 20.9555 16.8468 19.7433 18.3283L17.6064 16.1922C18.2926 15.2759 18.7595 14.1859 18.9291 13L21.9506 13.0001ZM13.0011 2.04948C17.725 2.51902 21.4815 6.27589 21.9506 10.9999L18.9291 10.9998C18.4905 7.93452 16.0661 5.50992 13.001 5.07103L13.0011 2.04948Z" fill="currentColor"/>`,
"donut-chart": `<path d="M10.9999 2.04938L11 4.06188C7.05371 4.55396 4 7.92036 4 12C4 16.4183 7.58172 20 12 20C13.8487 20 15.5509 19.3729 16.9055 18.3199L18.3289 19.7428C16.605 21.1536 14.4014 22 12 22C6.47715 22 2 17.5228 2 12C2 6.81468 5.94662 2.55115 10.9999 2.04938ZM21.9506 13.0001C21.7509 15.0111 20.9555 16.8468 19.7433 18.3283L18.3199 16.9055C19.1801 15.799 19.756 14.4606 19.9381 12.9999L21.9506 13.0001ZM13.0011 2.04948C17.725 2.51902 21.4815 6.27589 21.9506 10.9999L19.9381 11C19.4869 7.38162 16.6192 4.51364 13.001 4.062L13.0011 2.04948Z" fill="currentColor"/>`, "donut-chart": `<path d="M10.9999 2.04938L11 4.06188C7.05371 4.55396 4 7.92036 4 12C4 16.4183 7.58172 20 12 20C13.8487 20 15.5509 19.3729 16.9055 18.3199L18.3289 19.7428C16.605 21.1536 14.4014 22 12 22C6.47715 22 2 17.5228 2 12C2 6.81468 5.94662 2.55115 10.9999 2.04938ZM21.9506 13.0001C21.7509 15.0111 20.9555 16.8468 19.7433 18.3283L18.3199 16.9055C19.1801 15.799 19.756 14.4606 19.9381 12.9999L21.9506 13.0001ZM13.0011 2.04948C17.725 2.51902 21.4815 6.27589 21.9506 10.9999L19.9381 11C19.4869 7.38162 16.6192 4.51364 13.001 4.062L13.0011 2.04948Z" fill="currentColor"/>`,
"download-cloud": `<path d="M1 14.5C1 12.1716 2.22429 10.1291 4.06426 8.9812C4.56469 5.044 7.92686 2 12 2C16.0731 2 19.4353 5.044 19.9357 8.9812C21.7757 10.1291 23 12.1716 23 14.5C23 17.9216 20.3562 20.7257 17 20.9811L7 21C3.64378 20.7257 1 17.9216 1 14.5ZM16.8483 18.9868C19.1817 18.8093 21 16.8561 21 14.5C21 12.927 20.1884 11.4962 18.8771 10.6781L18.0714 10.1754L17.9517 9.23338C17.5735 6.25803 15.0288 4 12 4C8.97116 4 6.42647 6.25803 6.0483 9.23338L5.92856 10.1754L5.12288 10.6781C3.81156 11.4962 3 12.927 3 14.5C3 16.8561 4.81833 18.8093 7.1517 18.9868L7.325 19H16.675L16.8483 18.9868ZM13 12H16L12 17L8 12H11V8H13V12Z" fill="currentColor"/>`, "donut-chart-fill": `<path d="M10.9999 2.04938L11 5.07088C7.6077 5.55612 5 8.47352 5 12C5 15.866 8.13401 19 12 19C13.5723 19 15.0236 18.4816 16.1922 17.6064L18.3289 19.7428C16.605 21.1536 14.4014 22 12 22C6.47715 22 2 17.5228 2 12C2 6.81468 5.94662 2.55115 10.9999 2.04938ZM21.9506 13.0001C21.7509 15.0111 20.9555 16.8468 19.7433 18.3283L17.6064 16.1922C18.2926 15.2759 18.7595 14.1859 18.9291 13L21.9506 13.0001ZM13.0011 2.04948C17.725 2.51902 21.4815 6.27589 21.9506 10.9999L18.9291 10.9998C18.4905 7.93452 16.0661 5.50992 13.001 5.07103L13.0011 2.04948Z" fill="currentColor"/>`,
"download": `<path d="M3 19H21V21H3V19ZM13 13.1716L19.0711 7.1005L20.4853 8.51472L12 17L3.51472 8.51472L4.92893 7.1005L11 13.1716V2H13V13.1716Z" fill="currentColor"/>`, "download": `<path d="M3 19H21V21H3V19ZM13 13.1716L19.0711 7.1005L20.4853 8.51472L12 17L3.51472 8.51472L4.92893 7.1005L11 13.1716V2H13V13.1716Z" fill="currentColor"/>`,
"download-cloud": `<path d="M1 14.5C1 12.1716 2.22429 10.1291 4.06426 8.9812C4.56469 5.044 7.92686 2 12 2C16.0731 2 19.4353 5.044 19.9357 8.9812C21.7757 10.1291 23 12.1716 23 14.5C23 17.9216 20.3562 20.7257 17 20.9811L7 21C3.64378 20.7257 1 17.9216 1 14.5ZM16.8483 18.9868C19.1817 18.8093 21 16.8561 21 14.5C21 12.927 20.1884 11.4962 18.8771 10.6781L18.0714 10.1754L17.9517 9.23338C17.5735 6.25803 15.0288 4 12 4C8.97116 4 6.42647 6.25803 6.0483 9.23338L5.92856 10.1754L5.12288 10.6781C3.81156 11.4962 3 12.927 3 14.5C3 16.8561 4.81833 18.8093 7.1517 18.9868L7.325 19H16.675L16.8483 18.9868ZM13 12H16L12 17L8 12H11V8H13V12Z" fill="currentColor"/>`,
"drag-move-2": `<path d="M11 11V5.82843L9.17157 7.65685L7.75736 6.24264L12 2L16.2426 6.24264L14.8284 7.65685L13 5.82843V11H18.1716L16.3431 9.17157L17.7574 7.75736L22 12L17.7574 16.2426L16.3431 14.8284L18.1716 13H13V18.1716L14.8284 16.3431L16.2426 17.7574L12 22L7.75736 17.7574L9.17157 16.3431L11 18.1716V13H5.82843L7.65685 14.8284L6.24264 16.2426L2 12L6.24264 7.75736L7.65685 9.17157L5.82843 11H11Z" fill="currentColor"/>`, "drag-move-2": `<path d="M11 11V5.82843L9.17157 7.65685L7.75736 6.24264L12 2L16.2426 6.24264L14.8284 7.65685L13 5.82843V11H18.1716L16.3431 9.17157L17.7574 7.75736L22 12L17.7574 16.2426L16.3431 14.8284L18.1716 13H13V18.1716L14.8284 16.3431L16.2426 17.7574L12 22L7.75736 17.7574L9.17157 16.3431L11 18.1716V13H5.82843L7.65685 14.8284L6.24264 16.2426L2 12L6.24264 7.75736L7.65685 9.17157L5.82843 11H11Z" fill="currentColor"/>`,
"draggable": `<path d="M8.5 7C9.32843 7 10 6.32843 10 5.5C10 4.67157 9.32843 4 8.5 4C7.67157 4 7 4.67157 7 5.5C7 6.32843 7.67157 7 8.5 7ZM8.5 13.5C9.32843 13.5 10 12.8284 10 12C10 11.1716 9.32843 10.5 8.5 10.5C7.67157 10.5 7 11.1716 7 12C7 12.8284 7.67157 13.5 8.5 13.5ZM10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17C9.32843 17 10 17.6716 10 18.5ZM15.5 7C16.3284 7 17 6.32843 17 5.5C17 4.67157 16.3284 4 15.5 4C14.6716 4 14 4.67157 14 5.5C14 6.32843 14.6716 7 15.5 7ZM17 12C17 12.8284 16.3284 13.5 15.5 13.5C14.6716 13.5 14 12.8284 14 12C14 11.1716 14.6716 10.5 15.5 10.5C16.3284 10.5 17 11.1716 17 12ZM15.5 20C16.3284 20 17 19.3284 17 18.5C17 17.6716 16.3284 17 15.5 17C14.6716 17 14 17.6716 14 18.5C14 19.3284 14.6716 20 15.5 20Z" fill="currentColor"/>`, "draggable": `<path d="M8.5 7C9.32843 7 10 6.32843 10 5.5C10 4.67157 9.32843 4 8.5 4C7.67157 4 7 4.67157 7 5.5C7 6.32843 7.67157 7 8.5 7ZM8.5 13.5C9.32843 13.5 10 12.8284 10 12C10 11.1716 9.32843 10.5 8.5 10.5C7.67157 10.5 7 11.1716 7 12C7 12.8284 7.67157 13.5 8.5 13.5ZM10 18.5C10 19.3284 9.32843 20 8.5 20C7.67157 20 7 19.3284 7 18.5C7 17.6716 7.67157 17 8.5 17C9.32843 17 10 17.6716 10 18.5ZM15.5 7C16.3284 7 17 6.32843 17 5.5C17 4.67157 16.3284 4 15.5 4C14.6716 4 14 4.67157 14 5.5C14 6.32843 14.6716 7 15.5 7ZM17 12C17 12.8284 16.3284 13.5 15.5 13.5C14.6716 13.5 14 12.8284 14 12C14 11.1716 14.6716 10.5 15.5 10.5C16.3284 10.5 17 11.1716 17 12ZM15.5 20C16.3284 20 17 19.3284 17 18.5C17 17.6716 16.3284 17 15.5 17C14.6716 17 14 17.6716 14 18.5C14 19.3284 14.6716 20 15.5 20Z" fill="currentColor"/>`,
"edit-2": `<path d="M5 18.89H6.41421L15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89ZM21 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L9.24264 18.89H21V20.89ZM15.7279 6.74785L17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785Z" fill="currentColor"/>`,
"edit": `<path d="M6.41421 15.89L16.5563 5.74785L15.1421 4.33363L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6473L14.435 2.21231C14.8256 1.82179 15.4587 1.82179 15.8492 2.21231L18.6777 5.04074C19.0682 5.43126 19.0682 6.06443 18.6777 6.45495L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z" fill="currentColor"/>`, "edit": `<path d="M6.41421 15.89L16.5563 5.74785L15.1421 4.33363L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6473L14.435 2.21231C14.8256 1.82179 15.4587 1.82179 15.8492 2.21231L18.6777 5.04074C19.0682 5.43126 19.0682 6.06443 18.6777 6.45495L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z" fill="currentColor"/>`,
"edit-2": `<path d="M5 18.89H6.41421L15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89ZM21 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L9.24264 18.89H21V20.89ZM15.7279 6.74785L17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785Z" fill="currentColor"/>`,
"emotion-happy": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM7 13H9C9 14.6569 10.3431 16 12 16C13.6569 16 15 14.6569 15 13H17C17 15.7614 14.7614 18 12 18C9.23858 18 7 15.7614 7 13ZM8 11C7.17157 11 6.5 10.3284 6.5 9.5C6.5 8.67157 7.17157 8 8 8C8.82843 8 9.5 8.67157 9.5 9.5C9.5 10.3284 8.82843 11 8 11ZM16 11C15.1716 11 14.5 10.3284 14.5 9.5C14.5 8.67157 15.1716 8 16 8C16.8284 8 17.5 8.67157 17.5 9.5C17.5 10.3284 16.8284 11 16 11Z" fill="currentColor"/>`, "emotion-happy": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM7 13H9C9 14.6569 10.3431 16 12 16C13.6569 16 15 14.6569 15 13H17C17 15.7614 14.7614 18 12 18C9.23858 18 7 15.7614 7 13ZM8 11C7.17157 11 6.5 10.3284 6.5 9.5C6.5 8.67157 7.17157 8 8 8C8.82843 8 9.5 8.67157 9.5 9.5C9.5 10.3284 8.82843 11 8 11ZM16 11C15.1716 11 14.5 10.3284 14.5 9.5C14.5 8.67157 15.1716 8 16 8C16.8284 8 17.5 8.67157 17.5 9.5C17.5 10.3284 16.8284 11 16 11Z" fill="currentColor"/>`,
"equalizer-2": `<path d="M5 7C5 6.17157 5.67157 5.5 6.5 5.5C7.32843 5.5 8 6.17157 8 7C8 7.82843 7.32843 8.5 6.5 8.5C5.67157 8.5 5 7.82843 5 7ZM6.5 3.5C4.567 3.5 3 5.067 3 7C3 8.933 4.567 10.5 6.5 10.5C8.433 10.5 10 8.933 10 7C10 5.067 8.433 3.5 6.5 3.5ZM12 8H20V6H12V8ZM16 17C16 16.1716 16.6716 15.5 17.5 15.5C18.3284 15.5 19 16.1716 19 17C19 17.8284 18.3284 18.5 17.5 18.5C16.6716 18.5 16 17.8284 16 17ZM17.5 13.5C15.567 13.5 14 15.067 14 17C14 18.933 15.567 20.5 17.5 20.5C19.433 20.5 21 18.933 21 17C21 15.067 19.433 13.5 17.5 13.5ZM4 16V18H12V16H4Z" fill="currentColor"/>`, "equalizer-2": `<path d="M5 7C5 6.17157 5.67157 5.5 6.5 5.5C7.32843 5.5 8 6.17157 8 7C8 7.82843 7.32843 8.5 6.5 8.5C5.67157 8.5 5 7.82843 5 7ZM6.5 3.5C4.567 3.5 3 5.067 3 7C3 8.933 4.567 10.5 6.5 10.5C8.433 10.5 10 8.933 10 7C10 5.067 8.433 3.5 6.5 3.5ZM12 8H20V6H12V8ZM16 17C16 16.1716 16.6716 15.5 17.5 15.5C18.3284 15.5 19 16.1716 19 17C19 17.8284 18.3284 18.5 17.5 18.5C16.6716 18.5 16 17.8284 16 17ZM17.5 13.5C15.567 13.5 14 15.067 14 17C14 18.933 15.567 20.5 17.5 20.5C19.433 20.5 21 18.933 21 17C21 15.067 19.433 13.5 17.5 13.5ZM4 16V18H12V16H4Z" fill="currentColor"/>`,
"error-warning": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z" fill="currentColor"/>`, "error-warning": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z" fill="currentColor"/>`,
@@ -85,16 +85,16 @@ export const iconSpriteData = {
"external-link": `<path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z" fill="currentColor"/>`, "external-link": `<path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z" fill="currentColor"/>`,
"eye": `<path d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z" fill="currentColor"/>`, "eye": `<path d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z" fill="currentColor"/>`,
"eye-off": `<path d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z" fill="currentColor"/>`, "eye-off": `<path d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z" fill="currentColor"/>`,
"file": `<path d="M9 2.00318V2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8L9 2.00318ZM5.82918 8H9V4.83086L5.82918 8ZM11 4V9C11 9.55228 10.5523 10 10 10H5V20H19V4H11Z" fill="currentColor"/>`,
"file-add": `<path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 11V8H13V11H16V13H13V16H11V13H8V11H11Z" fill="currentColor"/>`, "file-add": `<path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 11V8H13V11H16V13H13V16H11V13H8V11H11Z" fill="currentColor"/>`,
"file-check-fill": `<path d="M20.9998 7L16 2H3.9985C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H12.3414C12.1203 21.3744 12 20.7013 12 20C12 16.6863 14.6863 14 18 14C19.0928 14 20.1174 14.2922 20.9999 14.8026L20.9998 7ZM14.4646 19.4647L18.0001 23.0002L22.9498 18.0505L21.5356 16.6362L18.0001 20.1718L15.8788 18.0505L14.4646 19.4647Z" fill="currentColor"/>`,
"file-check": `<path d="M12 20V22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9998 7V14H19V8H15V4H5V20H12ZM14.4646 19.4647L18.0001 23.0002L22.9498 18.0505L21.5356 16.6362L18.0001 20.1718L15.8788 18.0505L14.4646 19.4647Z" fill="currentColor"/>`, "file-check": `<path d="M12 20V22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9998 7V14H19V8H15V4H5V20H12ZM14.4646 19.4647L18.0001 23.0002L22.9498 18.0505L21.5356 16.6362L18.0001 20.1718L15.8788 18.0505L14.4646 19.4647Z" fill="currentColor"/>`,
"file-check-fill": `<path d="M20.9998 7L16 2H3.9985C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H12.3414C12.1203 21.3744 12 20.7013 12 20C12 16.6863 14.6863 14 18 14C19.0928 14 20.1174 14.2922 20.9999 14.8026L20.9998 7ZM14.4646 19.4647L18.0001 23.0002L22.9498 18.0505L21.5356 16.6362L18.0001 20.1718L15.8788 18.0505L14.4646 19.4647Z" fill="currentColor"/>`,
"file-code": `<path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM17.6569 12L14.1213 15.5355L12.7071 14.1213L14.8284 12L12.7071 9.87868L14.1213 8.46447L17.6569 12ZM6.34315 12L9.87868 8.46447L11.2929 9.87868L9.17157 12L11.2929 14.1213L9.87868 15.5355L6.34315 12Z" fill="currentColor"/>`, "file-code": `<path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM17.6569 12L14.1213 15.5355L12.7071 14.1213L14.8284 12L12.7071 9.87868L14.1213 8.46447L17.6569 12ZM6.34315 12L9.87868 8.46447L11.2929 9.87868L9.17157 12L11.2929 14.1213L9.87868 15.5355L6.34315 12Z" fill="currentColor"/>`,
"file-copy-2": `<path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6ZM7 11H13V13H7V11ZM7 15H13V17H7V15Z" fill="currentColor"/>`,
"file-copy": `<path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z" fill="currentColor"/>`, "file-copy": `<path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z" fill="currentColor"/>`,
"file-copy-2": `<path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6ZM7 11H13V13H7V11ZM7 15H13V17H7V15Z" fill="currentColor"/>`,
"file-download": `<path d="M13 12H16L12 16L8 12H11V8H13V12ZM15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z" fill="currentColor"/>`, "file-download": `<path d="M13 12H16L12 16L8 12H11V8H13V12ZM15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z" fill="currentColor"/>`,
"file-edit": `<path d="M21 6.75736L19 8.75736V4H10V9H5V20H19V17.2426L21 15.2426V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V6.75736ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761Z" fill="currentColor"/>`, "file-edit": `<path d="M21 6.75736L19 8.75736V4H10V9H5V20H19V17.2426L21 15.2426V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V6.75736ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761Z" fill="currentColor"/>`,
"file-image": `<path d="M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z" fill="currentColor"/>`, "file-image": `<path d="M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z" fill="currentColor"/>`,
"file": `<path d="M9 2.00318V2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8L9 2.00318ZM5.82918 8H9V4.83086L5.82918 8ZM11 4V9C11 9.55228 10.5523 10 10 10H5V20H19V4H11Z" fill="currentColor"/>`,
"file-list-2": `<path d="M20 22H4C3.44772 22 3 21.5523 3 21V3C3 2.44772 3.44772 2 4 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22ZM19 20V4H5V20H19ZM8 7H16V9H8V7ZM8 11H16V13H8V11ZM8 15H13V17H8V15Z" fill="currentColor"/>`, "file-list-2": `<path d="M20 22H4C3.44772 22 3 21.5523 3 21V3C3 2.44772 3.44772 2 4 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22ZM19 20V4H5V20H19ZM8 7H16V9H8V7ZM8 11H16V13H8V11ZM8 15H13V17H8V15Z" fill="currentColor"/>`,
"file-music": `<path d="M16 8V10H13V14.5C13 15.8807 11.8807 17 10.5 17C9.11929 17 8 15.8807 8 14.5C8 13.1193 9.11929 12 10.5 12C10.6712 12 10.8384 12.0172 11 12.05V8H15V4H5V20H19V8H16ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z" fill="currentColor"/>`, "file-music": `<path d="M16 8V10H13V14.5C13 15.8807 11.8807 17 10.5 17C9.11929 17 8 15.8807 8 14.5C8 13.1193 9.11929 12 10.5 12C10.6712 12 10.8384 12.0172 11 12.05V8H15V4H5V20H19V8H16ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z" fill="currentColor"/>`,
"file-pdf": `<path d="M12 16H8V8H12C14.2091 8 16 9.79086 16 12C16 14.2091 14.2091 16 12 16ZM10 10V14H12C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10H10ZM15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z" fill="currentColor"/>`, "file-pdf": `<path d="M12 16H8V8H12C14.2091 8 16 9.79086 16 12C16 14.2091 14.2091 16 12 16ZM10 10V14H12C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10H10ZM15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z" fill="currentColor"/>`,
@@ -104,18 +104,18 @@ export const iconSpriteData = {
"file-video": `<path d="M15 4V8H19V20H5V4H15ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM15.0008 11.667L10.1219 8.41435C10.0562 8.37054 9.979 8.34717 9.9 8.34717C9.6791 8.34717 9.5 8.52625 9.5 8.74717V15.2524C9.5 15.3314 9.5234 15.4086 9.5672 15.4743C9.6897 15.6581 9.9381 15.7078 10.1219 15.5852L15.0008 12.3326C15.0447 12.3033 15.0824 12.2656 15.1117 12.2217C15.2343 12.0379 15.1846 11.7895 15.0008 11.667Z" fill="currentColor"/>`, "file-video": `<path d="M15 4V8H19V20H5V4H15ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM15.0008 11.667L10.1219 8.41435C10.0562 8.37054 9.979 8.34717 9.9 8.34717C9.6791 8.34717 9.5 8.52625 9.5 8.74717V15.2524C9.5 15.3314 9.5234 15.4086 9.5672 15.4743C9.6897 15.6581 9.9381 15.7078 10.1219 15.5852L15.0008 12.3326C15.0447 12.3033 15.0824 12.2656 15.1117 12.2217C15.2343 12.0379 15.1846 11.7895 15.0008 11.667Z" fill="currentColor"/>`,
"flashlight": `<path d="M13 9H21L11 24V15H4L13 0V9ZM11 11V7.22063L7.53238 13H13V17.3944L17.263 11H11Z" fill="currentColor"/>`, "flashlight": `<path d="M13 9H21L11 24V15H4L13 0V9ZM11 11V7.22063L7.53238 13H13V17.3944L17.263 11H11Z" fill="currentColor"/>`,
"flask": `<path d="M15.9994 2V4H14.9994V7.24291C14.9994 8.40051 15.2506 9.54432 15.7357 10.5954L20.017 19.8714C20.3641 20.6236 20.0358 21.5148 19.2836 21.8619C19.0865 21.9529 18.8721 22 18.655 22H5.34375C4.51532 22 3.84375 21.3284 3.84375 20.5C3.84375 20.2829 3.89085 20.0685 3.98181 19.8714L8.26306 10.5954C8.74816 9.54432 8.99939 8.40051 8.99939 7.24291V4H7.99939V2H15.9994ZM13.3873 10.0012H10.6115C10.5072 10.3644 10.3823 10.7221 10.2371 11.0724L10.079 11.4335L6.12439 20H17.8734L13.9198 11.4335C13.7054 10.9691 13.5276 10.4902 13.3873 10.0012ZM10.9994 7.24291C10.9994 7.49626 10.9898 7.7491 10.9706 8.00087H13.0282C13.0189 7.87982 13.0119 7.75852 13.0072 7.63704L12.9994 7.24291V4H10.9994V7.24291Z" fill="currentColor"/>`, "flask": `<path d="M15.9994 2V4H14.9994V7.24291C14.9994 8.40051 15.2506 9.54432 15.7357 10.5954L20.017 19.8714C20.3641 20.6236 20.0358 21.5148 19.2836 21.8619C19.0865 21.9529 18.8721 22 18.655 22H5.34375C4.51532 22 3.84375 21.3284 3.84375 20.5C3.84375 20.2829 3.89085 20.0685 3.98181 19.8714L8.26306 10.5954C8.74816 9.54432 8.99939 8.40051 8.99939 7.24291V4H7.99939V2H15.9994ZM13.3873 10.0012H10.6115C10.5072 10.3644 10.3823 10.7221 10.2371 11.0724L10.079 11.4335L6.12439 20H17.8734L13.9198 11.4335C13.7054 10.9691 13.5276 10.4902 13.3873 10.0012ZM10.9994 7.24291C10.9994 7.49626 10.9898 7.7491 10.9706 8.00087H13.0282C13.0189 7.87982 13.0119 7.75852 13.0072 7.63704L12.9994 7.24291V4H10.9994V7.24291Z" fill="currentColor"/>`,
"folder-3-fill": `<path d="M22 8V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7H21C21.5523 7 22 7.44772 22 8ZM12.4142 5H2V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z" fill="currentColor"/>`, "folder": `<path d="M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z" fill="currentColor"/>`,
"folder-3": `<path d="M12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM4 7V19H20V7H4Z" fill="currentColor"/>`, "folder-3": `<path d="M12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM4 7V19H20V7H4Z" fill="currentColor"/>`,
"folder-3-fill": `<path d="M22 8V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7H21C21.5523 7 22 7.44772 22 8ZM12.4142 5H2V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z" fill="currentColor"/>`,
"folder-6": `<path d="M2 4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21L3 21C2.45 21 2 20.55 2 20V4ZM10.5858 6L9.58579 5H4V7H9.58579L10.5858 6ZM4 9V19L20 19V7H12.4142L10.4142 9H4Z" fill="currentColor"/>`, "folder-6": `<path d="M2 4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21L3 21C2.45 21 2 20.55 2 20V4ZM10.5858 6L9.58579 5H4V7H9.58579L10.5858 6ZM4 9V19L20 19V7H12.4142L10.4142 9H4Z" fill="currentColor"/>`,
"folder-add": `<path d="M12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM4 5V19H20V7H11.5858L9.58579 5H4ZM11 12V9H13V12H16V14H13V17H11V14H8V12H11Z" fill="currentColor"/>`, "folder-add": `<path d="M12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM4 5V19H20V7H11.5858L9.58579 5H4ZM11 12V9H13V12H16V14H13V17H11V14H8V12H11Z" fill="currentColor"/>`,
"folder": `<path d="M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z" fill="currentColor"/>`,
"folder-open-fill": `<path d="M3 21C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H20C20.5523 5 21 5.44772 21 6V9H4V18.996L6 11H22.5L20.1894 20.2425C20.0781 20.6877 19.6781 21 19.2192 21H3Z" fill="currentColor"/>`,
"folder-open": `<path d="M3 21C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H20C20.5523 5 21 5.44772 21 6V9H19V7H11.5858L9.58579 5H4V16.998L5.5 11H22.5L20.1894 20.2425C20.0781 20.6877 19.6781 21 19.2192 21H3ZM19.9384 13H7.06155L5.56155 19H18.4384L19.9384 13Z" fill="currentColor"/>`, "folder-open": `<path d="M3 21C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H20C20.5523 5 21 5.44772 21 6V9H19V7H11.5858L9.58579 5H4V16.998L5.5 11H22.5L20.1894 20.2425C20.0781 20.6877 19.6781 21 19.2192 21H3ZM19.9384 13H7.06155L5.56155 19H18.4384L19.9384 13Z" fill="currentColor"/>`,
"folder-open-fill": `<path d="M3 21C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H20C20.5523 5 21 5.44772 21 6V9H4V18.996L6 11H22.5L20.1894 20.2425C20.0781 20.6877 19.6781 21 19.2192 21H3Z" fill="currentColor"/>`,
"folder-received": `<path d="M22 13H20V7H11.5858L9.58579 5H4V19H13V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V13ZM20 17H23V19H20V22.5L15 18L20 13.5V17Z" fill="currentColor"/>`, "folder-received": `<path d="M22 13H20V7H11.5858L9.58579 5H4V19H13V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V13ZM20 17H23V19H20V22.5L15 18L20 13.5V17Z" fill="currentColor"/>`,
"folder-shared": `<path d="M22 13H20V7H11.5858L9.58579 5H4V19H13V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V13ZM18 17V13.5L23 18L18 22.5V19H15V17H18Z" fill="currentColor"/>`, "folder-shared": `<path d="M22 13H20V7H11.5858L9.58579 5H4V19H13V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V13ZM18 17V13.5L23 18L18 22.5V19H15V17H18Z" fill="currentColor"/>`,
"folders": `<path d="M6 7V4C6 3.44772 6.44772 3 7 3H13.4142L15.4142 5H21C21.5523 5 22 5.44772 22 6V16C22 16.5523 21.5523 17 21 17H18V20C18 20.5523 17.5523 21 17 21H3C2.44772 21 2 20.5523 2 20V8C2 7.44772 2.44772 7 3 7H6ZM6 9H4V19H16V17H6V9ZM8 5V15H20V7H14.5858L12.5858 5H8Z" fill="currentColor"/>`, "folders": `<path d="M6 7V4C6 3.44772 6.44772 3 7 3H13.4142L15.4142 5H21C21.5523 5 22 5.44772 22 6V16C22 16.5523 21.5523 17 21 17H18V20C18 20.5523 17.5523 21 17 21H3C2.44772 21 2 20.5523 2 20V8C2 7.44772 2.44772 7 3 7H6ZM6 9H4V19H16V17H6V9ZM8 5V15H20V7H14.5858L12.5858 5H8Z" fill="currentColor"/>`,
"fullscreen-exit": `<path d="M18 7H22V9H16V3H18V7ZM8 9H2V7H6V3H8V9ZM18 17V21H16V15H22V17H18ZM8 15V21H6V17H2V15H8Z" fill="currentColor"/>`,
"fullscreen": `<path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" fill="currentColor"/>`, "fullscreen": `<path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" fill="currentColor"/>`,
"fullscreen-exit": `<path d="M18 7H22V9H16V3H18V7ZM8 9H2V7H6V3H8V9ZM18 17V21H16V15H22V17H18ZM8 15V21H6V17H2V15H8Z" fill="currentColor"/>`,
"gamepad": `<path d="M17 4C20.3137 4 23 6.68629 23 10V14C23 17.3137 20.3137 20 17 20H7C3.68629 20 1 17.3137 1 14V10C1 6.68629 3.68629 4 7 4H17ZM17 6H7C4.8578 6 3.10892 7.68397 3.0049 9.80036L3 10V14C3 16.1422 4.68397 17.8911 6.80036 17.9951L7 18H17C19.1422 18 20.8911 16.316 20.9951 14.1996L21 14V10C21 7.8578 19.316 6.10892 17.1996 6.0049L17 6ZM10 9V11H12V13H9.999L10 15H8L7.999 13H6V11H8V9H10ZM18 13V15H16V13H18ZM16 9V11H14V9H16Z" fill="currentColor"/>`, "gamepad": `<path d="M17 4C20.3137 4 23 6.68629 23 10V14C23 17.3137 20.3137 20 17 20H7C3.68629 20 1 17.3137 1 14V10C1 6.68629 3.68629 4 7 4H17ZM17 6H7C4.8578 6 3.10892 7.68397 3.0049 9.80036L3 10V14C3 16.1422 4.68397 17.8911 6.80036 17.9951L7 18H17C19.1422 18 20.8911 16.316 20.9951 14.1996L21 14V10C21 7.8578 19.316 6.10892 17.1996 6.0049L17 6ZM10 9V11H12V13H9.999L10 15H8L7.999 13H6V11H8V9H10ZM18 13V15H16V13H18ZM16 9V11H14V9H16Z" fill="currentColor"/>`,
"git-branch": `<path d="M7.10508 15.2101C8.21506 15.6501 9 16.7334 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929C3.83481 8.41746 3 7.30622 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V11.9996C7.83566 11.3719 8.87439 11 10 11H14C15.3835 11 16.5482 10.0635 16.8949 8.78991C15.7849 8.34988 15 7.26661 15 6C15 4.34315 16.3431 3 18 3C19.6569 3 21 4.34315 21 6C21 7.3332 20.1303 8.46329 18.9274 8.85392C18.5222 11.2085 16.4703 13 14 13H10C8.61653 13 7.45179 13.9365 7.10508 15.2101ZM6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17ZM6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5ZM18 5C17.4477 5 17 5.44772 17 6C17 6.55228 17.4477 7 18 7C18.5523 7 19 6.55228 19 6C19 5.44772 18.5523 5 18 5Z" fill="currentColor"/>`, "git-branch": `<path d="M7.10508 15.2101C8.21506 15.6501 9 16.7334 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929C3.83481 8.41746 3 7.30622 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V11.9996C7.83566 11.3719 8.87439 11 10 11H14C15.3835 11 16.5482 10.0635 16.8949 8.78991C15.7849 8.34988 15 7.26661 15 6C15 4.34315 16.3431 3 18 3C19.6569 3 21 4.34315 21 6C21 7.3332 20.1303 8.46329 18.9274 8.85392C18.5222 11.2085 16.4703 13 14 13H10C8.61653 13 7.45179 13.9365 7.10508 15.2101ZM6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17ZM6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5ZM18 5C17.4477 5 17 5.44772 17 6C17 6.55228 17.4477 7 18 7C18.5523 7 19 6.55228 19 6C19 5.44772 18.5523 5 18 5Z" fill="currentColor"/>`,
"git-close-pull-request": `<path d="M6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5ZM3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V15.1707C8.16519 15.5825 9 16.6938 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929C3.83481 8.41746 3 7.30622 3 6ZM15.2929 3.29289C15.6834 2.90237 16.3166 2.90237 16.7071 3.29289L18 4.58579L19.2929 3.29289C19.6834 2.90237 20.3166 2.90237 20.7071 3.29289C21.0976 3.68342 21.0976 4.31658 20.7071 4.70711L19.4142 6L20.7071 7.29289C21.0976 7.68342 21.0976 8.31658 20.7071 8.70711C20.3166 9.09763 19.6834 9.09763 19.2929 8.70711L18 7.41421L16.7071 8.70711C16.3166 9.09763 15.6834 9.09763 15.2929 8.70711C14.9024 8.31658 14.9024 7.68342 15.2929 7.29289L16.5858 6L15.2929 4.70711C14.9024 4.31658 14.9024 3.68342 15.2929 3.29289ZM18 10C18.5523 10 19 10.4477 19 11V15.1707C20.1652 15.5825 21 16.6938 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.6938 15.8348 15.5825 17 15.1707V11C17 10.4477 17.4477 10 18 10ZM6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17ZM18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17Z" fill="currentColor"/>`, "git-close-pull-request": `<path d="M6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5ZM3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V15.1707C8.16519 15.5825 9 16.6938 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929C3.83481 8.41746 3 7.30622 3 6ZM15.2929 3.29289C15.6834 2.90237 16.3166 2.90237 16.7071 3.29289L18 4.58579L19.2929 3.29289C19.6834 2.90237 20.3166 2.90237 20.7071 3.29289C21.0976 3.68342 21.0976 4.31658 20.7071 4.70711L19.4142 6L20.7071 7.29289C21.0976 7.68342 21.0976 8.31658 20.7071 8.70711C20.3166 9.09763 19.6834 9.09763 19.2929 8.70711L18 7.41421L16.7071 8.70711C16.3166 9.09763 15.6834 9.09763 15.2929 8.70711C14.9024 8.31658 14.9024 7.68342 15.2929 7.29289L16.5858 6L15.2929 4.70711C14.9024 4.31658 14.9024 3.68342 15.2929 3.29289ZM18 10C18.5523 10 19 10.4477 19 11V15.1707C20.1652 15.5825 21 16.6938 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.6938 15.8348 15.5825 17 15.1707V11C17 10.4477 17.4477 10 18 10ZM6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17ZM18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17Z" fill="currentColor"/>`,
@@ -124,8 +124,8 @@ export const iconSpriteData = {
"git-pr-draft": `<path d="M5 6C5 5.44772 5.44772 5 6 5C6.55228 5 7 5.44772 7 6C7 6.55228 6.55228 7 6 7C5.44772 7 5 6.55228 5 6ZM6 3C4.34315 3 3 4.34315 3 6C3 7.30622 3.83481 8.41746 5 8.82929V15.1707C3.83481 15.5825 3 16.6938 3 18C3 19.6569 4.34315 21 6 21C7.65685 21 9 19.6569 9 18C9 16.6938 8.16519 15.5825 7 15.1707V8.82929C8.16519 8.41746 9 7.30622 9 6C9 4.34315 7.65685 3 6 3ZM5 18C5 17.4477 5.44772 17 6 17C6.55228 17 7 17.4477 7 18C7 18.5523 6.55228 19 6 19C5.44772 19 5 18.5523 5 18ZM18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17ZM15 18C15 16.3431 16.3431 15 18 15C19.6569 15 21 16.3431 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18ZM18 7.5C18.8284 7.5 19.5 6.82843 19.5 6C19.5 5.17157 18.8284 4.5 18 4.5C17.1716 4.5 16.5 5.17157 16.5 6C16.5 6.82843 17.1716 7.5 18 7.5ZM19.5 11.5C19.5 12.3284 18.8284 13 18 13C17.1716 13 16.5 12.3284 16.5 11.5C16.5 10.6716 17.1716 10 18 10C18.8284 10 19.5 10.6716 19.5 11.5Z" fill="currentColor"/>`, "git-pr-draft": `<path d="M5 6C5 5.44772 5.44772 5 6 5C6.55228 5 7 5.44772 7 6C7 6.55228 6.55228 7 6 7C5.44772 7 5 6.55228 5 6ZM6 3C4.34315 3 3 4.34315 3 6C3 7.30622 3.83481 8.41746 5 8.82929V15.1707C3.83481 15.5825 3 16.6938 3 18C3 19.6569 4.34315 21 6 21C7.65685 21 9 19.6569 9 18C9 16.6938 8.16519 15.5825 7 15.1707V8.82929C8.16519 8.41746 9 7.30622 9 6C9 4.34315 7.65685 3 6 3ZM5 18C5 17.4477 5.44772 17 6 17C6.55228 17 7 17.4477 7 18C7 18.5523 6.55228 19 6 19C5.44772 19 5 18.5523 5 18ZM18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17ZM15 18C15 16.3431 16.3431 15 18 15C19.6569 15 21 16.3431 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18ZM18 7.5C18.8284 7.5 19.5 6.82843 19.5 6C19.5 5.17157 18.8284 4.5 18 4.5C17.1716 4.5 16.5 5.17157 16.5 6C16.5 6.82843 17.1716 7.5 18 7.5ZM19.5 11.5C19.5 12.3284 18.8284 13 18 13C17.1716 13 16.5 12.3284 16.5 11.5C16.5 10.6716 17.1716 10 18 10C18.8284 10 19.5 10.6716 19.5 11.5Z" fill="currentColor"/>`,
"git-pull-request": `<path d="M15 5H17C18.1046 5 19 5.89543 19 7V15.1707C20.1652 15.5825 21 16.6938 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.6938 15.8348 15.5825 17 15.1707V7H15V10L10.5 6L15 2V5ZM5 8.82929C3.83481 8.41746 3 7.30622 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V15.1707C8.16519 15.5825 9 16.6938 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929ZM6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7ZM6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19ZM18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19Z" fill="currentColor"/>`, "git-pull-request": `<path d="M15 5H17C18.1046 5 19 5.89543 19 7V15.1707C20.1652 15.5825 21 16.6938 21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.6938 15.8348 15.5825 17 15.1707V7H15V10L10.5 6L15 2V5ZM5 8.82929C3.83481 8.41746 3 7.30622 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V15.1707C8.16519 15.5825 9 16.6938 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929ZM6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7ZM6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19ZM18 19C18.5523 19 19 18.5523 19 18C19 17.4477 18.5523 17 18 17C17.4477 17 17 17.4477 17 18C17 18.5523 17.4477 19 18 19Z" fill="currentColor"/>`,
"git-repository": `<path d="M13 21V23.5L10 21.5L7 23.5V21H6.5C4.567 21 3 19.433 3 17.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V20C21 20.5523 20.5523 21 20 21H13ZM13 19H19V16H6.5C5.67157 16 5 16.6716 5 17.5C5 18.3284 5.67157 19 6.5 19H7V17H13V19ZM19 14V4H6V14.0354C6.1633 14.0121 6.33024 14 6.5 14H19ZM7 5H9V7H7V5ZM7 8H9V10H7V8ZM7 11H9V13H7V11Z" fill="currentColor"/>`, "git-repository": `<path d="M13 21V23.5L10 21.5L7 23.5V21H6.5C4.567 21 3 19.433 3 17.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V20C21 20.5523 20.5523 21 20 21H13ZM13 19H19V16H6.5C5.67157 16 5 16.6716 5 17.5C5 18.3284 5.67157 19 6.5 19H7V17H13V19ZM19 14V4H6V14.0354C6.1633 14.0121 6.33024 14 6.5 14H19ZM7 5H9V7H7V5ZM7 8H9V10H7V8ZM7 11H9V13H7V11Z" fill="currentColor"/>`,
"github-fill": `<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z" fill="currentColor"/>`,
"github": `<path d="M5.88401 18.6533C5.58404 18.4526 5.32587 18.1975 5.0239 17.8369C4.91473 17.7065 4.47283 17.1524 4.55811 17.2583C4.09533 16.6833 3.80296 16.417 3.50156 16.3089C2.9817 16.1225 2.7114 15.5499 2.89784 15.0301C3.08428 14.5102 3.65685 14.2399 4.17672 14.4263C4.92936 14.6963 5.43847 15.1611 6.12425 16.0143C6.03025 15.8974 6.46364 16.441 6.55731 16.5529C6.74784 16.7804 6.88732 16.9182 6.99629 16.9911C7.20118 17.1283 7.58451 17.1874 8.14709 17.1311C8.17065 16.7489 8.24136 16.3783 8.34919 16.0358C5.38097 15.3104 3.70116 13.3952 3.70116 9.63971C3.70116 8.40085 4.0704 7.28393 4.75917 6.3478C4.5415 5.45392 4.57433 4.37284 5.06092 3.15636C5.1725 2.87739 5.40361 2.66338 5.69031 2.57352C5.77242 2.54973 5.81791 2.53915 5.89878 2.52673C6.70167 2.40343 7.83573 2.69705 9.31449 3.62336C10.181 3.41879 11.0885 3.315 12.0012 3.315C12.9129 3.315 13.8196 3.4186 14.6854 3.62277C16.1619 2.69 17.2986 2.39649 18.1072 2.52651C18.1919 2.54013 18.2645 2.55783 18.3249 2.57766C18.6059 2.66991 18.8316 2.88179 18.9414 3.15636C19.4279 4.37256 19.4608 5.45344 19.2433 6.3472C19.9342 7.28337 20.3012 8.39208 20.3012 9.63971C20.3012 13.3968 18.627 15.3048 15.6588 16.032C15.7837 16.447 15.8496 16.9105 15.8496 17.4121C15.8496 18.0765 15.8471 18.711 15.8424 19.4225C15.8412 19.6127 15.8397 19.8159 15.8375 20.1281C16.2129 20.2109 16.5229 20.5077 16.6031 20.9089C16.7114 21.4504 16.3602 21.9773 15.8186 22.0856C14.6794 22.3134 13.8353 21.5538 13.8353 20.5611C13.8353 20.4708 13.836 20.3417 13.8375 20.1145C13.8398 19.8015 13.8412 19.599 13.8425 19.4094C13.8471 18.7019 13.8496 18.0716 13.8496 17.4121C13.8496 16.7148 13.6664 16.2602 13.4237 16.051C12.7627 15.4812 13.0977 14.3973 13.965 14.2999C16.9314 13.9666 18.3012 12.8177 18.3012 9.63971C18.3012 8.68508 17.9893 7.89571 17.3881 7.23559C17.1301 6.95233 17.0567 6.54659 17.199 6.19087C17.3647 5.77663 17.4354 5.23384 17.2941 4.57702L17.2847 4.57968C16.7928 4.71886 16.1744 5.0198 15.4261 5.5285C15.182 5.69438 14.8772 5.74401 14.5932 5.66413C13.7729 5.43343 12.8913 5.315 12.0012 5.315C11.111 5.315 10.2294 5.43343 9.40916 5.66413C9.12662 5.74359 8.82344 5.69492 8.57997 5.53101C7.8274 5.02439 7.2056 4.72379 6.71079 4.58376C6.56735 5.23696 6.63814 5.77782 6.80336 6.19087C6.94565 6.54659 6.87219 6.95233 6.61423 7.23559C6.01715 7.8912 5.70116 8.69376 5.70116 9.63971C5.70116 12.8116 7.07225 13.9683 10.023 14.2999C10.8883 14.3971 11.2246 15.4769 10.5675 16.0482C10.3751 16.2156 10.1384 16.7802 10.1384 17.4121V20.5611C10.1384 21.5474 9.30356 22.2869 8.17878 22.09C7.63476 21.9948 7.27093 21.4766 7.36613 20.9326C7.43827 20.5204 7.75331 20.2116 8.13841 20.1276V19.1381C7.22829 19.1994 6.47656 19.0498 5.88401 18.6533Z" fill="currentColor"/>`, "github": `<path d="M5.88401 18.6533C5.58404 18.4526 5.32587 18.1975 5.0239 17.8369C4.91473 17.7065 4.47283 17.1524 4.55811 17.2583C4.09533 16.6833 3.80296 16.417 3.50156 16.3089C2.9817 16.1225 2.7114 15.5499 2.89784 15.0301C3.08428 14.5102 3.65685 14.2399 4.17672 14.4263C4.92936 14.6963 5.43847 15.1611 6.12425 16.0143C6.03025 15.8974 6.46364 16.441 6.55731 16.5529C6.74784 16.7804 6.88732 16.9182 6.99629 16.9911C7.20118 17.1283 7.58451 17.1874 8.14709 17.1311C8.17065 16.7489 8.24136 16.3783 8.34919 16.0358C5.38097 15.3104 3.70116 13.3952 3.70116 9.63971C3.70116 8.40085 4.0704 7.28393 4.75917 6.3478C4.5415 5.45392 4.57433 4.37284 5.06092 3.15636C5.1725 2.87739 5.40361 2.66338 5.69031 2.57352C5.77242 2.54973 5.81791 2.53915 5.89878 2.52673C6.70167 2.40343 7.83573 2.69705 9.31449 3.62336C10.181 3.41879 11.0885 3.315 12.0012 3.315C12.9129 3.315 13.8196 3.4186 14.6854 3.62277C16.1619 2.69 17.2986 2.39649 18.1072 2.52651C18.1919 2.54013 18.2645 2.55783 18.3249 2.57766C18.6059 2.66991 18.8316 2.88179 18.9414 3.15636C19.4279 4.37256 19.4608 5.45344 19.2433 6.3472C19.9342 7.28337 20.3012 8.39208 20.3012 9.63971C20.3012 13.3968 18.627 15.3048 15.6588 16.032C15.7837 16.447 15.8496 16.9105 15.8496 17.4121C15.8496 18.0765 15.8471 18.711 15.8424 19.4225C15.8412 19.6127 15.8397 19.8159 15.8375 20.1281C16.2129 20.2109 16.5229 20.5077 16.6031 20.9089C16.7114 21.4504 16.3602 21.9773 15.8186 22.0856C14.6794 22.3134 13.8353 21.5538 13.8353 20.5611C13.8353 20.4708 13.836 20.3417 13.8375 20.1145C13.8398 19.8015 13.8412 19.599 13.8425 19.4094C13.8471 18.7019 13.8496 18.0716 13.8496 17.4121C13.8496 16.7148 13.6664 16.2602 13.4237 16.051C12.7627 15.4812 13.0977 14.3973 13.965 14.2999C16.9314 13.9666 18.3012 12.8177 18.3012 9.63971C18.3012 8.68508 17.9893 7.89571 17.3881 7.23559C17.1301 6.95233 17.0567 6.54659 17.199 6.19087C17.3647 5.77663 17.4354 5.23384 17.2941 4.57702L17.2847 4.57968C16.7928 4.71886 16.1744 5.0198 15.4261 5.5285C15.182 5.69438 14.8772 5.74401 14.5932 5.66413C13.7729 5.43343 12.8913 5.315 12.0012 5.315C11.111 5.315 10.2294 5.43343 9.40916 5.66413C9.12662 5.74359 8.82344 5.69492 8.57997 5.53101C7.8274 5.02439 7.2056 4.72379 6.71079 4.58376C6.56735 5.23696 6.63814 5.77782 6.80336 6.19087C6.94565 6.54659 6.87219 6.95233 6.61423 7.23559C6.01715 7.8912 5.70116 8.69376 5.70116 9.63971C5.70116 12.8116 7.07225 13.9683 10.023 14.2999C10.8883 14.3971 11.2246 15.4769 10.5675 16.0482C10.3751 16.2156 10.1384 16.7802 10.1384 17.4121V20.5611C10.1384 21.5474 9.30356 22.2869 8.17878 22.09C7.63476 21.9948 7.27093 21.4766 7.36613 20.9326C7.43827 20.5204 7.75331 20.2116 8.13841 20.1276V19.1381C7.22829 19.1994 6.47656 19.0498 5.88401 18.6533Z" fill="currentColor"/>`,
"github-fill": `<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z" fill="currentColor"/>`,
"global": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM9.71002 19.6674C8.74743 17.6259 8.15732 15.3742 8.02731 13H4.06189C4.458 16.1765 6.71639 18.7747 9.71002 19.6674ZM10.0307 13C10.1811 15.4388 10.8778 17.7297 12 19.752C13.1222 17.7297 13.8189 15.4388 13.9693 13H10.0307ZM19.9381 13H15.9727C15.8427 15.3742 15.2526 17.6259 14.29 19.6674C17.2836 18.7747 19.542 16.1765 19.9381 13ZM4.06189 11H8.02731C8.15732 8.62577 8.74743 6.37407 9.71002 4.33256C6.71639 5.22533 4.458 7.8235 4.06189 11ZM10.0307 11H13.9693C13.8189 8.56122 13.1222 6.27025 12 4.24799C10.8778 6.27025 10.1811 8.56122 10.0307 11ZM14.29 4.33256C15.2526 6.37407 15.8427 8.62577 15.9727 11H19.9381C19.542 7.8235 17.2836 5.22533 14.29 4.33256Z" fill="currentColor"/>`, "global": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM9.71002 19.6674C8.74743 17.6259 8.15732 15.3742 8.02731 13H4.06189C4.458 16.1765 6.71639 18.7747 9.71002 19.6674ZM10.0307 13C10.1811 15.4388 10.8778 17.7297 12 19.752C13.1222 17.7297 13.8189 15.4388 13.9693 13H10.0307ZM19.9381 13H15.9727C15.8427 15.3742 15.2526 17.6259 14.29 19.6674C17.2836 18.7747 19.542 16.1765 19.9381 13ZM4.06189 11H8.02731C8.15732 8.62577 8.74743 6.37407 9.71002 4.33256C6.71639 5.22533 4.458 7.8235 4.06189 11ZM10.0307 11H13.9693C13.8189 8.56122 13.1222 6.27025 12 4.24799C10.8778 6.27025 10.1811 8.56122 10.0307 11ZM14.29 4.33256C15.2526 6.37407 15.8427 8.62577 15.9727 11H19.9381C19.542 7.8235 17.2836 5.22533 14.29 4.33256Z" fill="currentColor"/>`,
"graduation-cap": `<path d="M4 11.3333L0 9L12 2L24 9V17.5H22V10.1667L20 11.3333V18.0113L19.7774 18.2864C17.9457 20.5499 15.1418 22 12 22C8.85817 22 6.05429 20.5499 4.22263 18.2864L4 18.0113V11.3333ZM6 12.5V17.2917C7.46721 18.954 9.61112 20 12 20C14.3889 20 16.5328 18.954 18 17.2917V12.5L12 16L6 12.5ZM3.96927 9L12 13.6846L20.0307 9L12 4.31541L3.96927 9Z" fill="currentColor"/>`, "graduation-cap": `<path d="M4 11.3333L0 9L12 2L24 9V17.5H22V10.1667L20 11.3333V18.0113L19.7774 18.2864C17.9457 20.5499 15.1418 22 12 22C8.85817 22 6.05429 20.5499 4.22263 18.2864L4 18.0113V11.3333ZM6 12.5V17.2917C7.46721 18.954 9.61112 20 12 20C14.3889 20 16.5328 18.954 18 17.2917V12.5L12 16L6 12.5ZM3.96927 9L12 13.6846L20.0307 9L12 4.31541L3.96927 9Z" fill="currentColor"/>`,
"hammer": `<path d="M20 2C20.5523 2 21 2.44772 21 3V8C21 8.55228 20.5523 9 20 9H15V22C15 22.5523 14.5523 23 14 23H10C9.44772 23 9 22.5523 9 22V9H3.5C2.94772 9 2.5 8.55228 2.5 8V5.61803C2.5 5.23926 2.714 4.893 3.05279 4.72361L8.5 2H20ZM15 4H8.97214L4.5 6.23607V7H11V21H13V7H15V4ZM19 4H17V7H19V4Z" fill="currentColor"/>`, "hammer": `<path d="M20 2C20.5523 2 21 2.44772 21 3V8C21 8.55228 20.5523 9 20 9H15V22C15 22.5523 14.5523 23 14 23H10C9.44772 23 9 22.5523 9 22V9H3.5C2.94772 9 2.5 8.55228 2.5 8V5.61803C2.5 5.23926 2.714 4.893 3.05279 4.72361L8.5 2H20ZM15 4H8.97214L4.5 6.23607V7H11V21H13V7H15V4ZM19 4H17V7H19V4Z" fill="currentColor"/>`,
@@ -133,12 +133,12 @@ export const iconSpriteData = {
"history": `<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12H4C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C9.25022 4 6.82447 5.38734 5.38451 7.50024L8 7.5V9.5H2V3.5H4L3.99989 5.99918C5.82434 3.57075 8.72873 2 12 2ZM13 7L12.9998 11.585L16.2426 14.8284L14.8284 16.2426L10.9998 12.413L11 7H13Z" fill="currentColor"/>`, "history": `<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12H4C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C9.25022 4 6.82447 5.38734 5.38451 7.50024L8 7.5V9.5H2V3.5H4L3.99989 5.99918C5.82434 3.57075 8.72873 2 12 2ZM13 7L12.9998 11.585L16.2426 14.8284L14.8284 16.2426L10.9998 12.413L11 7H13Z" fill="currentColor"/>`,
"home": `<path d="M21 20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V9.48907C3 9.18048 3.14247 8.88917 3.38606 8.69972L11.3861 2.47749C11.7472 2.19663 12.2528 2.19663 12.6139 2.47749L20.6139 8.69972C20.8575 8.88917 21 9.18048 21 9.48907V20ZM19 19V9.97815L12 4.53371L5 9.97815V19H19Z" fill="currentColor"/>`, "home": `<path d="M21 20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V9.48907C3 9.18048 3.14247 8.88917 3.38606 8.69972L11.3861 2.47749C11.7472 2.19663 12.2528 2.19663 12.6139 2.47749L20.6139 8.69972C20.8575 8.88917 21 9.18048 21 9.48907V20ZM19 19V9.97815L12 4.53371L5 9.97815V19H19Z" fill="currentColor"/>`,
"home-office": `<path d="M12.6727 1.61162 20.7999 9H17.8267L12 3.70302 6 9.15757V19.0001H11V21.0001H5C4.44772 21.0001 4 20.5524 4 20.0001V11.0001L1 11.0001 11.3273 1.61162C11.7087 1.26488 12.2913 1.26488 12.6727 1.61162ZM14 11H23V18H14V11ZM16 13V16H21V13H16ZM24 21H13V19H24V21Z" fill="currentColor"/>`, "home-office": `<path d="M12.6727 1.61162 20.7999 9H17.8267L12 3.70302 6 9.15757V19.0001H11V21.0001H5C4.44772 21.0001 4 20.5524 4 20.0001V11.0001L1 11.0001 11.3273 1.61162C11.7087 1.26488 12.2913 1.26488 12.6727 1.61162ZM14 11H23V18H14V11ZM16 13V16H21V13H16ZM24 21H13V19H24V21Z" fill="currentColor"/>`,
"hourglass-fill": `<path d="M6 4H4V2H20V4H18V6C18 7.61543 17.1838 8.91468 16.1561 9.97667C15.4532 10.703 14.598 11.372 13.7309 12C14.598 12.628 15.4532 13.297 16.1561 14.0233C17.1838 15.0853 18 16.3846 18 18V20H20V22H4V20H6V18C6 16.3846 6.81616 15.0853 7.8439 14.0233C8.54682 13.297 9.40202 12.628 10.2691 12C9.40202 11.372 8.54682 10.703 7.8439 9.97667C6.81616 8.91468 6 7.61543 6 6V4ZM8 4V6C8 6.68514 8.26026 7.33499 8.77131 8H15.2287C15.7397 7.33499 16 6.68514 16 6V4H8ZM12 13.2219C10.9548 13.9602 10.008 14.663 9.2811 15.4142C9.09008 15.6116 8.92007 15.8064 8.77131 16H15.2287C15.0799 15.8064 14.9099 15.6116 14.7189 15.4142C13.992 14.663 13.0452 13.9602 12 13.2219Z" fill="currentColor"/>`,
"hourglass": `<path d="M6 4H4V2H20V4H18V6C18 7.61543 17.1838 8.91468 16.1561 9.97667C15.4532 10.703 14.598 11.372 13.7309 12C14.598 12.628 15.4532 13.297 16.1561 14.0233C17.1838 15.0853 18 16.3846 18 18V20H20V22H4V20H6V18C6 16.3846 6.81616 15.0853 7.8439 14.0233C8.54682 13.297 9.40202 12.628 10.2691 12C9.40202 11.372 8.54682 10.703 7.8439 9.97667C6.81616 8.91468 6 7.61543 6 6V4ZM8 4V6C8 6.88457 8.43384 7.71032 9.2811 8.58583C10.008 9.33699 10.9548 10.0398 12 10.7781C13.0452 10.0398 13.992 9.33699 14.7189 8.58583C15.5662 7.71032 16 6.88457 16 6V4H8ZM12 13.2219C10.9548 13.9602 10.008 14.663 9.2811 15.4142C8.43384 16.2897 8 17.1154 8 18V20H16V18C16 17.1154 15.5662 16.2897 14.7189 15.4142C13.992 14.663 13.0452 13.9602 12 13.2219Z" fill="currentColor"/>`, "hourglass": `<path d="M6 4H4V2H20V4H18V6C18 7.61543 17.1838 8.91468 16.1561 9.97667C15.4532 10.703 14.598 11.372 13.7309 12C14.598 12.628 15.4532 13.297 16.1561 14.0233C17.1838 15.0853 18 16.3846 18 18V20H20V22H4V20H6V18C6 16.3846 6.81616 15.0853 7.8439 14.0233C8.54682 13.297 9.40202 12.628 10.2691 12C9.40202 11.372 8.54682 10.703 7.8439 9.97667C6.81616 8.91468 6 7.61543 6 6V4ZM8 4V6C8 6.88457 8.43384 7.71032 9.2811 8.58583C10.008 9.33699 10.9548 10.0398 12 10.7781C13.0452 10.0398 13.992 9.33699 14.7189 8.58583C15.5662 7.71032 16 6.88457 16 6V4H8ZM12 13.2219C10.9548 13.9602 10.008 14.663 9.2811 15.4142C8.43384 16.2897 8 17.1154 8 18V20H16V18C16 17.1154 15.5662 16.2897 14.7189 15.4142C13.992 14.663 13.0452 13.9602 12 13.2219Z" fill="currentColor"/>`,
"hourglass-fill": `<path d="M6 4H4V2H20V4H18V6C18 7.61543 17.1838 8.91468 16.1561 9.97667C15.4532 10.703 14.598 11.372 13.7309 12C14.598 12.628 15.4532 13.297 16.1561 14.0233C17.1838 15.0853 18 16.3846 18 18V20H20V22H4V20H6V18C6 16.3846 6.81616 15.0853 7.8439 14.0233C8.54682 13.297 9.40202 12.628 10.2691 12C9.40202 11.372 8.54682 10.703 7.8439 9.97667C6.81616 8.91468 6 7.61543 6 6V4ZM8 4V6C8 6.68514 8.26026 7.33499 8.77131 8H15.2287C15.7397 7.33499 16 6.68514 16 6V4H8ZM12 13.2219C10.9548 13.9602 10.008 14.663 9.2811 15.4142C9.09008 15.6116 8.92007 15.8064 8.77131 16H15.2287C15.0799 15.8064 14.9099 15.6116 14.7189 15.4142C13.992 14.663 13.0452 13.9602 12 13.2219Z" fill="currentColor"/>`,
"image-download": `<path d="M21 15V19H24L20 23L16 19H19V15H21ZM21.0078 3C21.5555 3 21.9999 3.44482 22 3.99316V13H20V5H4V18.999L14 9L17 12V14.8291L14 11.8281L6.82715 19H14V21H2.99219C2.44451 21 2.00013 20.5552 2 20.0068V3.99316C2.00013 3.44463 2.45577 3 2.99219 3H21.0078ZM8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7Z" fill="currentColor"/>`, "image-download": `<path d="M21 15V19H24L20 23L16 19H19V15H21ZM21.0078 3C21.5555 3 21.9999 3.44482 22 3.99316V13H20V5H4V18.999L14 9L17 12V14.8291L14 11.8281L6.82715 19H14V21H2.99219C2.44451 21 2.00013 20.5552 2 20.0068V3.99316C2.00013 3.44463 2.45577 3 2.99219 3H21.0078ZM8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7Z" fill="currentColor"/>`,
"inbox-archive": `<path d="M20 3L22 7V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.00353L4 3H20ZM20 9H4V19H20V9ZM13 10V14H16L12 18L8 14H11V10H13ZM18.7639 5H5.23656L4.23744 7H19.7639L18.7639 5Z" fill="currentColor"/>`, "inbox-archive": `<path d="M20 3L22 7V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.00353L4 3H20ZM20 9H4V19H20V9ZM13 10V14H16L12 18L8 14H11V10H13ZM18.7639 5H5.23656L4.23744 7H19.7639L18.7639 5Z" fill="currentColor"/>`,
"inbox-unarchive-fill": `<path d="M20 3L22 7V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.00353L4 3H20ZM12 10L8 14H11V18H13V14H16L12 10ZM18.764 5H5.236L4.237 7H19.764L18.764 5Z" fill="currentColor"/>`,
"inbox-unarchive": `<path d="M20 3L22 7V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.00353L4 3H20ZM20 9H4V19H20V9ZM12 10L16 14H13V18H11V14H8L12 10ZM18.764 5H5.236L4.237 7H19.764L18.764 5Z" fill="currentColor"/>`, "inbox-unarchive": `<path d="M20 3L22 7V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.00353L4 3H20ZM20 9H4V19H20V9ZM12 10L16 14H13V18H11V14H8L12 10ZM18.764 5H5.236L4.237 7H19.764L18.764 5Z" fill="currentColor"/>`,
"inbox-unarchive-fill": `<path d="M20 3L22 7V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.00353L4 3H20ZM12 10L8 14H11V18H13V14H16L12 10ZM18.764 5H5.236L4.237 7H19.764L18.764 5Z" fill="currentColor"/>`,
"information": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 7H13V9H11V7ZM11 11H13V17H11V11Z" fill="currentColor"/>`, "information": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 7H13V9H11V7ZM11 11H13V17H11V11Z" fill="currentColor"/>`,
"key": `<path d="M12.917 13C12.441 15.8377 9.973 18 7 18C3.68629 18 1 15.3137 1 12C1 8.68629 3.68629 6 7 6C9.973 6 12.441 8.16229 12.917 11H23V13H21V17H19V13H17V17H15V13H12.917ZM7 16C9.20914 16 11 14.2091 11 12C11 9.79086 9.20914 8 7 8C4.79086 8 3 9.79086 3 12C3 14.2091 4.79086 16 7 16Z" fill="currentColor"/>`, "key": `<path d="M12.917 13C12.441 15.8377 9.973 18 7 18C3.68629 18 1 15.3137 1 12C1 8.68629 3.68629 6 7 6C9.973 6 12.441 8.16229 12.917 11H23V13H21V17H19V13H17V17H15V13H12.917ZM7 16C9.20914 16 11 14.2091 11 12C11 9.79086 9.20914 8 7 8C4.79086 8 3 9.79086 3 12C3 14.2091 4.79086 16 7 16Z" fill="currentColor"/>`,
"layout-column": `<path d="M11 5H5V19H11V5ZM13 5V19H19V5H13ZM4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3Z" fill="currentColor"/>`, "layout-column": `<path d="M11 5H5V19H11V5ZM13 5V19H19V5H13ZM4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3Z" fill="currentColor"/>`,
@@ -150,10 +150,10 @@ export const iconSpriteData = {
"list-check-2": `<path d="M11 4H21V6H11V4ZM11 8H17V10H11V8ZM11 14H21V16H11V14ZM11 18H17V20H11V18ZM3 4H9V10H3V4ZM5 6V8H7V6H5ZM3 14H9V20H3V14ZM5 16V18H7V16H5Z" fill="currentColor"/>`, "list-check-2": `<path d="M11 4H21V6H11V4ZM11 8H17V10H11V8ZM11 14H21V16H11V14ZM11 18H17V20H11V18ZM3 4H9V10H3V4ZM5 6V8H7V6H5ZM3 14H9V20H3V14ZM5 16V18H7V16H5Z" fill="currentColor"/>`,
"list-check-3": `<path d="M8.00008 6V9H5.00008V6H8.00008ZM3.00008 4V11H10.0001V4H3.00008ZM13.0001 4H21.0001V6H13.0001V4ZM13.0001 11H21.0001V13H13.0001V11ZM13.0001 18H21.0001V20H13.0001V18ZM10.7072 16.2071L9.29297 14.7929L6.00008 18.0858L4.20718 16.2929L2.79297 17.7071L6.00008 20.9142L10.7072 16.2071Z" fill="currentColor"/>`, "list-check-3": `<path d="M8.00008 6V9H5.00008V6H8.00008ZM3.00008 4V11H10.0001V4H3.00008ZM13.0001 4H21.0001V6H13.0001V4ZM13.0001 11H21.0001V13H13.0001V11ZM13.0001 18H21.0001V20H13.0001V18ZM10.7072 16.2071L9.29297 14.7929L6.00008 18.0858L4.20718 16.2929L2.79297 17.7071L6.00008 20.9142L10.7072 16.2071Z" fill="currentColor"/>`,
"list-unordered": `<path d="M8 4H21V6H8V4ZM4.5 6.5C3.67157 6.5 3 5.82843 3 5C3 4.17157 3.67157 3.5 4.5 3.5C5.32843 3.5 6 4.17157 6 5C6 5.82843 5.32843 6.5 4.5 6.5ZM4.5 13.5C3.67157 13.5 3 12.8284 3 12C3 11.1716 3.67157 10.5 4.5 10.5C5.32843 10.5 6 11.1716 6 12C6 12.8284 5.32843 13.5 4.5 13.5ZM4.5 20.4C3.67157 20.4 3 19.7284 3 18.9C3 18.0716 3.67157 17.4 4.5 17.4C5.32843 17.4 6 18.0716 6 18.9C6 19.7284 5.32843 20.4 4.5 20.4ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z" fill="currentColor"/>`, "list-unordered": `<path d="M8 4H21V6H8V4ZM4.5 6.5C3.67157 6.5 3 5.82843 3 5C3 4.17157 3.67157 3.5 4.5 3.5C5.32843 3.5 6 4.17157 6 5C6 5.82843 5.32843 6.5 4.5 6.5ZM4.5 13.5C3.67157 13.5 3 12.8284 3 12C3 11.1716 3.67157 10.5 4.5 10.5C5.32843 10.5 6 11.1716 6 12C6 12.8284 5.32843 13.5 4.5 13.5ZM4.5 20.4C3.67157 20.4 3 19.7284 3 18.9C3 18.0716 3.67157 17.4 4.5 17.4C5.32843 17.4 6 18.0716 6 18.9C6 19.7284 5.32843 20.4 4.5 20.4ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z" fill="currentColor"/>`,
"loader-4": `<path d="M18.364 5.63604L16.9497 7.05025C15.683 5.7835 13.933 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12H21C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C14.4853 3 16.7353 4.00736 18.364 5.63604Z" fill="currentColor"/>`,
"loader": `<path d="M11.9995 2C12.5518 2 12.9995 2.44772 12.9995 3V6C12.9995 6.55228 12.5518 7 11.9995 7C11.4472 7 10.9995 6.55228 10.9995 6V3C10.9995 2.44772 11.4472 2 11.9995 2ZM11.9995 17C12.5518 17 12.9995 17.4477 12.9995 18V21C12.9995 21.5523 12.5518 22 11.9995 22C11.4472 22 10.9995 21.5523 10.9995 21V18C10.9995 17.4477 11.4472 17 11.9995 17ZM20.6597 7C20.9359 7.47829 20.772 8.08988 20.2937 8.36602L17.6956 9.86602C17.2173 10.1422 16.6057 9.97829 16.3296 9.5C16.0535 9.02171 16.2173 8.41012 16.6956 8.13398L19.2937 6.63397C19.772 6.35783 20.3836 6.52171 20.6597 7ZM7.66935 14.5C7.94549 14.9783 7.78161 15.5899 7.30332 15.866L4.70525 17.366C4.22695 17.6422 3.61536 17.4783 3.33922 17C3.06308 16.5217 3.22695 15.9101 3.70525 15.634L6.30332 14.134C6.78161 13.8578 7.3932 14.0217 7.66935 14.5ZM20.6597 17C20.3836 17.4783 19.772 17.6422 19.2937 17.366L16.6956 15.866C16.2173 15.5899 16.0535 14.9783 16.3296 14.5C16.6057 14.0217 17.2173 13.8578 17.6956 14.134L20.2937 15.634C20.772 15.9101 20.9359 16.5217 20.6597 17ZM7.66935 9.5C7.3932 9.97829 6.78161 10.1422 6.30332 9.86602L3.70525 8.36602C3.22695 8.08988 3.06308 7.47829 3.33922 7C3.61536 6.52171 4.22695 6.35783 4.70525 6.63397L7.30332 8.13398C7.78161 8.41012 7.94549 9.02171 7.66935 9.5Z" fill="currentColor"/>`, "loader": `<path d="M11.9995 2C12.5518 2 12.9995 2.44772 12.9995 3V6C12.9995 6.55228 12.5518 7 11.9995 7C11.4472 7 10.9995 6.55228 10.9995 6V3C10.9995 2.44772 11.4472 2 11.9995 2ZM11.9995 17C12.5518 17 12.9995 17.4477 12.9995 18V21C12.9995 21.5523 12.5518 22 11.9995 22C11.4472 22 10.9995 21.5523 10.9995 21V18C10.9995 17.4477 11.4472 17 11.9995 17ZM20.6597 7C20.9359 7.47829 20.772 8.08988 20.2937 8.36602L17.6956 9.86602C17.2173 10.1422 16.6057 9.97829 16.3296 9.5C16.0535 9.02171 16.2173 8.41012 16.6956 8.13398L19.2937 6.63397C19.772 6.35783 20.3836 6.52171 20.6597 7ZM7.66935 14.5C7.94549 14.9783 7.78161 15.5899 7.30332 15.866L4.70525 17.366C4.22695 17.6422 3.61536 17.4783 3.33922 17C3.06308 16.5217 3.22695 15.9101 3.70525 15.634L6.30332 14.134C6.78161 13.8578 7.3932 14.0217 7.66935 14.5ZM20.6597 17C20.3836 17.4783 19.772 17.6422 19.2937 17.366L16.6956 15.866C16.2173 15.5899 16.0535 14.9783 16.3296 14.5C16.6057 14.0217 17.2173 13.8578 17.6956 14.134L20.2937 15.634C20.772 15.9101 20.9359 16.5217 20.6597 17ZM7.66935 9.5C7.3932 9.97829 6.78161 10.1422 6.30332 9.86602L3.70525 8.36602C3.22695 8.08988 3.06308 7.47829 3.33922 7C3.61536 6.52171 4.22695 6.35783 4.70525 6.63397L7.30332 8.13398C7.78161 8.41012 7.94549 9.02171 7.66935 9.5Z" fill="currentColor"/>`,
"lock-2": `<path d="M6 8V7C6 3.68629 8.68629 1 12 1C15.3137 1 18 3.68629 18 7V8H20C20.5523 8 21 8.44772 21 9V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V9C3 8.44772 3.44772 8 4 8H6ZM19 10H5V20H19V10ZM11 15.7324C10.4022 15.3866 10 14.7403 10 14C10 12.8954 10.8954 12 12 12C13.1046 12 14 12.8954 14 14C14 14.7403 13.5978 15.3866 13 15.7324V18H11V15.7324ZM8 8H16V7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7V8Z" fill="currentColor"/>`, "loader-4": `<path d="M18.364 5.63604L16.9497 7.05025C15.683 5.7835 13.933 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12H21C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C14.4853 3 16.7353 4.00736 18.364 5.63604Z" fill="currentColor"/>`,
"lock": `<path d="M19 10H20C20.5523 10 21 10.4477 21 11V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V11C3 10.4477 3.44772 10 4 10H5V9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9V10ZM5 12V20H19V12H5ZM11 14H13V18H11V14ZM17 10V9C17 6.23858 14.7614 4 12 4C9.23858 4 7 6.23858 7 9V10H17Z" fill="currentColor"/>`, "lock": `<path d="M19 10H20C20.5523 10 21 10.4477 21 11V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V11C3 10.4477 3.44772 10 4 10H5V9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9V10ZM5 12V20H19V12H5ZM11 14H13V18H11V14ZM17 10V9C17 6.23858 14.7614 4 12 4C9.23858 4 7 6.23858 7 9V10H17Z" fill="currentColor"/>`,
"lock-2": `<path d="M6 8V7C6 3.68629 8.68629 1 12 1C15.3137 1 18 3.68629 18 7V8H20C20.5523 8 21 8.44772 21 9V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V9C3 8.44772 3.44772 8 4 8H6ZM19 10H5V20H19V10ZM11 15.7324C10.4022 15.3866 10 14.7403 10 14C10 12.8954 10.8954 12 12 12C13.1046 12 14 12.8954 14 14C14 14.7403 13.5978 15.3866 13 15.7324V18H11V15.7324ZM8 8H16V7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7V8Z" fill="currentColor"/>`,
"lock-unlock": `<path d="M7 10H20C20.5523 10 21 10.4477 21 11V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V11C3 10.4477 3.44772 10 4 10H5V9C5 5.13401 8.13401 2 12 2C14.7405 2 17.1131 3.5748 18.2624 5.86882L16.4731 6.76344C15.6522 5.12486 13.9575 4 12 4C9.23858 4 7 6.23858 7 9V10ZM5 12V20H19V12H5ZM10 15H14V17H10V15Z" fill="currentColor"/>`, "lock-unlock": `<path d="M7 10H20C20.5523 10 21 10.4477 21 11V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V11C3 10.4477 3.44772 10 4 10H5V9C5 5.13401 8.13401 2 12 2C14.7405 2 17.1131 3.5748 18.2624 5.86882L16.4731 6.76344C15.6522 5.12486 13.9575 4 12 4C9.23858 4 7 6.23858 7 9V10ZM5 12V20H19V12H5ZM10 15H14V17H10V15Z" fill="currentColor"/>`,
"loop-right-ai": `<path d="M22 12C22 17.5228 17.5228 22 12 22C8.72774 22 5.82382 20.4286 4 18.001V20.5H2V14.5H8V16.5H5.38477C6.82543 18.6137 9.25151 20 12 20C16.4183 20 20 16.4183 20 12H22ZM11.5293 8.31934C11.7059 7.8935 12.2943 7.89349 12.4707 8.31934L12.7236 8.93066C13.1556 9.97346 13.9615 10.8062 14.9746 11.2568L15.6924 11.5762C16.1026 11.759 16.1026 12.3562 15.6924 12.5391L14.9326 12.877C13.9449 13.3162 13.1534 14.1194 12.7139 15.1279L12.4668 15.6934C12.2864 16.1075 11.7137 16.1075 11.5332 15.6934L11.2871 15.1279C10.8476 14.1193 10.0552 13.3163 9.06738 12.877L8.30762 12.5391C7.89744 12.3562 7.89741 11.759 8.30762 11.5762L9.02539 11.2568C10.0385 10.8062 10.8445 9.97348 11.2764 8.93066L11.5293 8.31934ZM12 2C15.2723 2 18.1762 3.57144 20 5.99902V3.5H22V9.5H16V7.5H18.6152C17.1746 5.38634 14.7485 4 12 4C7.58172 4 4 7.58172 4 12H2C2 6.47715 6.47715 2 12 2Z" fill="currentColor"/>`, "loop-right-ai": `<path d="M22 12C22 17.5228 17.5228 22 12 22C8.72774 22 5.82382 20.4286 4 18.001V20.5H2V14.5H8V16.5H5.38477C6.82543 18.6137 9.25151 20 12 20C16.4183 20 20 16.4183 20 12H22ZM11.5293 8.31934C11.7059 7.8935 12.2943 7.89349 12.4707 8.31934L12.7236 8.93066C13.1556 9.97346 13.9615 10.8062 14.9746 11.2568L15.6924 11.5762C16.1026 11.759 16.1026 12.3562 15.6924 12.5391L14.9326 12.877C13.9449 13.3162 13.1534 14.1194 12.7139 15.1279L12.4668 15.6934C12.2864 16.1075 11.7137 16.1075 11.5332 15.6934L11.2871 15.1279C10.8476 14.1193 10.0552 13.3163 9.06738 12.877L8.30762 12.5391C7.89744 12.3562 7.89741 11.759 8.30762 11.5762L9.02539 11.2568C10.0385 10.8062 10.8445 9.97348 11.2764 8.93066L11.5293 8.31934ZM12 2C15.2723 2 18.1762 3.57144 20 5.99902V3.5H22V9.5H16V7.5H18.6152C17.1746 5.38634 14.7485 4 12 4C7.58172 4 4 7.58172 4 12H2C2 6.47715 6.47715 2 12 2Z" fill="currentColor"/>`,
"macbook": `<path d="M4 5V16H20V5H4ZM2 4.00748C2 3.45107 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44892 22 4.00748V18H2V4.00748ZM1 19H23V21H1V19Z" fill="currentColor"/>`, "macbook": `<path d="M4 5V16H20V5H4ZM2 4.00748C2 3.45107 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44892 22 4.00748V18H2V4.00748ZM1 19H23V21H1V19Z" fill="currentColor"/>`,
@@ -161,48 +161,49 @@ export const iconSpriteData = {
"menu-fold-2": `<path d="M4.40347 3.90332L2.98926 5.31753L6.17124 8.49951L2.98926 11.6815L4.40347 13.0957L8.99967 8.49951L4.40347 3.90332ZM20.9997 19.9995V17.9995H2.99967V19.9995H20.9997ZM20.9997 12.9995V10.9995H11.9997V12.9995H20.9997ZM20.9997 5.99951V3.99951H11.9997V5.99951H20.9997Z" fill="currentColor"/>`, "menu-fold-2": `<path d="M4.40347 3.90332L2.98926 5.31753L6.17124 8.49951L2.98926 11.6815L4.40347 13.0957L8.99967 8.49951L4.40347 3.90332ZM20.9997 19.9995V17.9995H2.99967V19.9995H20.9997ZM20.9997 12.9995V10.9995H11.9997V12.9995H20.9997ZM20.9997 5.99951V3.99951H11.9997V5.99951H20.9997Z" fill="currentColor"/>`,
"menu-search": `<path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z" fill="currentColor"/>`, "menu-search": `<path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z" fill="currentColor"/>`,
"mic": `<path d="M11.9998 3C10.3429 3 8.99976 4.34315 8.99976 6V10C8.99976 11.6569 10.3429 13 11.9998 13C13.6566 13 14.9998 11.6569 14.9998 10V6C14.9998 4.34315 13.6566 3 11.9998 3ZM11.9998 1C14.7612 1 16.9998 3.23858 16.9998 6V10C16.9998 12.7614 14.7612 15 11.9998 15C9.23833 15 6.99976 12.7614 6.99976 10V6C6.99976 3.23858 9.23833 1 11.9998 1ZM3.05469 11H5.07065C5.55588 14.3923 8.47329 17 11.9998 17C15.5262 17 18.4436 14.3923 18.9289 11H20.9448C20.4837 15.1716 17.1714 18.4839 12.9998 18.9451V23H10.9998V18.9451C6.82814 18.4839 3.51584 15.1716 3.05469 11Z" fill="currentColor"/>`, "mic": `<path d="M11.9998 3C10.3429 3 8.99976 4.34315 8.99976 6V10C8.99976 11.6569 10.3429 13 11.9998 13C13.6566 13 14.9998 11.6569 14.9998 10V6C14.9998 4.34315 13.6566 3 11.9998 3ZM11.9998 1C14.7612 1 16.9998 3.23858 16.9998 6V10C16.9998 12.7614 14.7612 15 11.9998 15C9.23833 15 6.99976 12.7614 6.99976 10V6C6.99976 3.23858 9.23833 1 11.9998 1ZM3.05469 11H5.07065C5.55588 14.3923 8.47329 17 11.9998 17C15.5262 17 18.4436 14.3923 18.9289 11H20.9448C20.4837 15.1716 17.1714 18.4839 12.9998 18.9451V23H10.9998V18.9451C6.82814 18.4839 3.51584 15.1716 3.05469 11Z" fill="currentColor"/>`,
"more-2-fill": `<path d="M12 3C10.9 3 10 3.9 10 5C10 6.1 10.9 7 12 7C13.1 7 14 6.1 14 5C14 3.9 13.1 3 12 3ZM12 17C10.9 17 10 17.9 10 19C10 20.1 10.9 21 12 21C13.1 21 14 20.1 14 19C14 17.9 13.1 17 12 17ZM12 10C10.9 10 10 10.9 10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10Z" fill="currentColor"/>`,
"more-2": `<path d="M12 3C11.175 3 10.5 3.675 10.5 4.5C10.5 5.325 11.175 6 12 6C12.825 6 13.5 5.325 13.5 4.5C13.5 3.675 12.825 3 12 3ZM12 18C11.175 18 10.5 18.675 10.5 19.5C10.5 20.325 11.175 21 12 21C12.825 21 13.5 20.325 13.5 19.5C13.5 18.675 12.825 18 12 18ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" fill="currentColor"/>`,
"more": `<path d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" fill="currentColor"/>`, "more": `<path d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" fill="currentColor"/>`,
"more-2": `<path d="M12 3C11.175 3 10.5 3.675 10.5 4.5C10.5 5.325 11.175 6 12 6C12.825 6 13.5 5.325 13.5 4.5C13.5 3.675 12.825 3 12 3ZM12 18C11.175 18 10.5 18.675 10.5 19.5C10.5 20.325 11.175 21 12 21C12.825 21 13.5 20.325 13.5 19.5C13.5 18.675 12.825 18 12 18ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" fill="currentColor"/>`,
"more-2-fill": `<path d="M12 3C10.9 3 10 3.9 10 5C10 6.1 10.9 7 12 7C13.1 7 14 6.1 14 5C14 3.9 13.1 3 12 3ZM12 17C10.9 17 10 17.9 10 19C10 20.1 10.9 21 12 21C13.1 21 14 20.1 14 19C14 17.9 13.1 17 12 17ZM12 10C10.9 10 10 10.9 10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10Z" fill="currentColor"/>`,
"music": `<path d="M12 13.5351V3H20V5H14V17C14 19.2091 12.2091 21 10 21C7.79086 21 6 19.2091 6 17C6 14.7909 7.79086 13 10 13C10.7286 13 11.4117 13.1948 12 13.5351ZM10 19C11.1046 19 12 18.1046 12 17C12 15.8954 11.1046 15 10 15C8.89543 15 8 15.8954 8 17C8 18.1046 8.89543 19 10 19Z" fill="currentColor"/>`, "music": `<path d="M12 13.5351V3H20V5H14V17C14 19.2091 12.2091 21 10 21C7.79086 21 6 19.2091 6 17C6 14.7909 7.79086 13 10 13C10.7286 13 11.4117 13.1948 12 13.5351ZM10 19C11.1046 19 12 18.1046 12 17C12 15.8954 11.1046 15 10 15C8.89543 15 8 15.8954 8 17C8 18.1046 8.89543 19 10 19Z" fill="currentColor"/>`,
"node-tree": `<path d="M10 2C10.5523 2 11 2.44772 11 3V7C11 7.55228 10.5523 8 10 8H8V10H13V9C13 8.44772 13.4477 8 14 8H20C20.5523 8 21 8.44772 21 9V13C21 13.5523 20.5523 14 20 14H14C13.4477 14 13 13.5523 13 13V12H8V18H13V17C13 16.4477 13.4477 16 14 16H20C20.5523 16 21 16.4477 21 17V21C21 21.5523 20.5523 22 20 22H14C13.4477 22 13 21.5523 13 21V20H7C6.44772 20 6 19.5523 6 19V8H4C3.44772 8 3 7.55228 3 7V3C3 2.44772 3.44772 2 4 2H10ZM19 18H15V20H19V18ZM19 10H15V12H19V10ZM9 4H5V6H9V4Z" fill="currentColor"/>`, "node-tree": `<path d="M10 2C10.5523 2 11 2.44772 11 3V7C11 7.55228 10.5523 8 10 8H8V10H13V9C13 8.44772 13.4477 8 14 8H20C20.5523 8 21 8.44772 21 9V13C21 13.5523 20.5523 14 20 14H14C13.4477 14 13 13.5523 13 13V12H8V18H13V17C13 16.4477 13.4477 16 14 16H20C20.5523 16 21 16.4477 21 17V21C21 21.5523 20.5523 22 20 22H14C13.4477 22 13 21.5523 13 21V20H7C6.44772 20 6 19.5523 6 19V8H4C3.44772 8 3 7.55228 3 7V3C3 2.44772 3.44772 2 4 2H10ZM19 18H15V20H19V18ZM19 10H15V12H19V10ZM9 4H5V6H9V4Z" fill="currentColor"/>`,
"notification-3": `<path d="M20 17H22V19H2V17H4V10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10V17ZM18 17V10C18 6.68629 15.3137 4 12 4C8.68629 4 6 6.68629 6 10V17H18ZM9 21H15V23H9V21Z" fill="currentColor"/>`, "notification-3": `<path d="M20 17H22V19H2V17H4V10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10V17ZM18 17V10C18 6.68629 15.3137 4 12 4C8.68629 4 6 6.68629 6 10V17H18ZM9 21H15V23H9V21Z" fill="currentColor"/>`,
"openchamber": `<polygon points="12 2.5 3.5 7.4 3.5 17.2 12 22.1 20.5 17.2 20.5 7.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><polyline points="3.5 7.4 12 12.3 20.5 7.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="12.3" x2="12" y2="22.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="m12 5.5 3.7 2.1L12 9.7 8.3 7.6 12 5.5Zm0 1.5-1 .6 1 .6 1-.6-1-.6Z" fill="currentColor" fill-rule="evenodd"/>`,
"palette": `<path d="M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z" fill="currentColor"/>`, "palette": `<path d="M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z" fill="currentColor"/>`,
"pause": `<path d="M6 5H8V19H6V5ZM16 5H18V19H16V5Z" fill="currentColor"/>`, "pause": `<path d="M6 5H8V19H6V5ZM16 5H18V19H16V5Z" fill="currentColor"/>`,
"pencil-ai-2": `<path d="M18.5293 15.3193C18.7058 14.8934 19.2942 14.8934 19.4707 15.3193L19.7236 15.9307C20.1556 16.9735 20.9615 17.8062 21.9746 18.2568L22.6914 18.5762C23.1022 18.7589 23.1022 19.3564 22.6914 19.5391L21.9326 19.877C20.9449 20.3163 20.1534 21.1194 19.7139 22.1279L19.4668 22.6934C19.2863 23.1075 18.7136 23.1075 18.5332 22.6934L18.2861 22.1279C17.8466 21.1194 17.0551 20.3163 16.0674 19.877L15.3076 19.5391C14.8974 19.3562 14.8974 18.759 15.3076 18.5762L16.0254 18.2568C17.0385 17.8062 17.8444 16.9735 18.2764 15.9307L18.5293 15.3193ZM16.4346 3.21193C16.8251 2.82141 17.4591 2.82141 17.8496 3.21193L20.6777 6.04103C21.0681 6.43157 21.0682 7.06464 20.6777 7.45509L7.24219 20.8897H3V16.6475L16.4346 3.21193ZM5 17.4756V18.8897H6.41406L15.7275 9.57618L14.3135 8.16212L5 17.4756ZM15.7275 6.74806L17.1426 8.16212L18.5566 6.74806L17.1426 5.334L15.7275 6.74806Z" fill="currentColor"/>`,
"pencil-ai": `<path d="M16.4356 3.21188C16.8261 2.82185 17.4592 2.82157 17.8496 3.21188L20.6777 6.04099C21.0681 6.43152 21.0682 7.06457 20.6777 7.45505L7.2422 20.8896H3.00001V16.6475L16.4356 3.21188ZM5.00001 17.4756V18.8896H6.41407L15.7276 9.57615L14.3135 8.16208L5.00001 17.4756ZM4.5293 1.3193C4.70583 0.893505 5.29418 0.893508 5.47071 1.3193L5.72364 1.93063C6.15555 2.97342 6.96155 3.80613 7.97462 4.2568L8.69239 4.57614C9.10267 4.75896 9.10262 5.35616 8.69239 5.53903L7.93263 5.87692C6.94497 6.3162 6.15339 7.11943 5.71387 8.1279L5.4668 8.69334C5.28636 9.10747 4.71366 9.10747 4.53321 8.69334L4.28614 8.1279C3.84661 7.11943 3.05506 6.3162 2.06739 5.87692L1.30762 5.53903C0.897483 5.35617 0.897435 4.75896 1.30762 4.57614L2.0254 4.2568C3.03845 3.80614 3.84446 2.97344 4.27637 1.93063L4.5293 1.3193ZM15.7276 6.74802L17.1426 8.16208L18.5567 6.74802L17.1426 5.33395L15.7276 6.74802Z" fill="currentColor"/>`,
"pencil": `<path d="M15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89H6.41421L15.7279 9.57627ZM17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785L17.1421 8.16206ZM7.24264 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L7.24264 20.89Z" fill="currentColor"/>`, "pencil": `<path d="M15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89H6.41421L15.7279 9.57627ZM17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785L17.1421 8.16206ZM7.24264 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L7.24264 20.89Z" fill="currentColor"/>`,
"pencil-ai": `<path d="M16.4356 3.21188C16.8261 2.82185 17.4592 2.82157 17.8496 3.21188L20.6777 6.04099C21.0681 6.43152 21.0682 7.06457 20.6777 7.45505L7.2422 20.8896H3.00001V16.6475L16.4356 3.21188ZM5.00001 17.4756V18.8896H6.41407L15.7276 9.57615L14.3135 8.16208L5.00001 17.4756ZM4.5293 1.3193C4.70583 0.893505 5.29418 0.893508 5.47071 1.3193L5.72364 1.93063C6.15555 2.97342 6.96155 3.80613 7.97462 4.2568L8.69239 4.57614C9.10267 4.75896 9.10262 5.35616 8.69239 5.53903L7.93263 5.87692C6.94497 6.3162 6.15339 7.11943 5.71387 8.1279L5.4668 8.69334C5.28636 9.10747 4.71366 9.10747 4.53321 8.69334L4.28614 8.1279C3.84661 7.11943 3.05506 6.3162 2.06739 5.87692L1.30762 5.53903C0.897483 5.35617 0.897435 4.75896 1.30762 4.57614L2.0254 4.2568C3.03845 3.80614 3.84446 2.97344 4.27637 1.93063L4.5293 1.3193ZM15.7276 6.74802L17.1426 8.16208L18.5567 6.74802L17.1426 5.33395L15.7276 6.74802Z" fill="currentColor"/>`,
"pencil-ai-2": `<path d="M18.5293 15.3193C18.7058 14.8934 19.2942 14.8934 19.4707 15.3193L19.7236 15.9307C20.1556 16.9735 20.9615 17.8062 21.9746 18.2568L22.6914 18.5762C23.1022 18.7589 23.1022 19.3564 22.6914 19.5391L21.9326 19.877C20.9449 20.3163 20.1534 21.1194 19.7139 22.1279L19.4668 22.6934C19.2863 23.1075 18.7136 23.1075 18.5332 22.6934L18.2861 22.1279C17.8466 21.1194 17.0551 20.3163 16.0674 19.877L15.3076 19.5391C14.8974 19.3562 14.8974 18.759 15.3076 18.5762L16.0254 18.2568C17.0385 17.8062 17.8444 16.9735 18.2764 15.9307L18.5293 15.3193ZM16.4346 3.21193C16.8251 2.82141 17.4591 2.82141 17.8496 3.21193L20.6777 6.04103C21.0681 6.43157 21.0682 7.06464 20.6777 7.45509L7.24219 20.8897H3V16.6475L16.4346 3.21193ZM5 17.4756V18.8897H6.41406L15.7275 9.57618L14.3135 8.16212L5 17.4756ZM15.7275 6.74806L17.1426 8.16212L18.5566 6.74806L17.1426 5.334L15.7275 6.74806Z" fill="currentColor"/>`,
"picture-in-picture-2": `<path d="M21 3C21.5523 3 22 3.44772 22 4V11H20V5H4V19H10V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM21 13C21.5523 13 22 13.4477 22 14V20C22 20.5523 21.5523 21 21 21H13C12.4477 21 12 20.5523 12 20V14C12 13.4477 12.4477 13 13 13H21ZM20 15H14V19H20V15ZM6.70711 6.29289L8.95689 8.54289L11 6.5V12H5.5L7.54289 9.95689L5.29289 7.70711L6.70711 6.29289Z" fill="currentColor"/>`, "picture-in-picture-2": `<path d="M21 3C21.5523 3 22 3.44772 22 4V11H20V5H4V19H10V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM21 13C21.5523 13 22 13.4477 22 14V20C22 20.5523 21.5523 21 21 21H13C12.4477 21 12 20.5523 12 20V14C12 13.4477 12.4477 13 13 13H21ZM20 15H14V19H20V15ZM6.70711 6.29289L8.95689 8.54289L11 6.5V12H5.5L7.54289 9.95689L5.29289 7.70711L6.70711 6.29289Z" fill="currentColor"/>`,
"pie-chart": `<path d="M9 2.4578V4.58152C6.06817 5.76829 4 8.64262 4 12C4 16.4183 7.58172 20 12 20C15.3574 20 18.2317 17.9318 19.4185 15H21.5422C20.2679 19.0571 16.4776 22 12 22C6.47715 22 2 17.5228 2 12C2 7.52236 4.94289 3.73207 9 2.4578ZM12 2C17.5228 2 22 6.47715 22 12C22 12.3375 21.9833 12.6711 21.9506 13H11V2.04938C11.3289 2.01672 11.6625 2 12 2ZM13 4.06189V11H19.9381C19.4869 7.38128 16.6187 4.51314 13 4.06189Z" fill="currentColor"/>`, "pie-chart": `<path d="M9 2.4578V4.58152C6.06817 5.76829 4 8.64262 4 12C4 16.4183 7.58172 20 12 20C15.3574 20 18.2317 17.9318 19.4185 15H21.5422C20.2679 19.0571 16.4776 22 12 22C6.47715 22 2 17.5228 2 12C2 7.52236 4.94289 3.73207 9 2.4578ZM12 2C17.5228 2 22 6.47715 22 12C22 12.3375 21.9833 12.6711 21.9506 13H11V2.04938C11.3289 2.01672 11.6625 2 12 2ZM13 4.06189V11H19.9381C19.4869 7.38128 16.6187 4.51314 13 4.06189Z" fill="currentColor"/>`,
"play": `<path d="M16.3944 12.0001L10 7.7371V16.263L16.3944 12.0001ZM19.376 12.4161L8.77735 19.4818C8.54759 19.635 8.23715 19.5729 8.08397 19.3432C8.02922 19.261 8 19.1645 8 19.0658V4.93433C8 4.65818 8.22386 4.43433 8.5 4.43433C8.59871 4.43433 8.69522 4.46355 8.77735 4.5183L19.376 11.584C19.6057 11.7372 19.6678 12.0477 19.5146 12.2774C19.478 12.3323 19.4309 12.3795 19.376 12.4161Z" fill="currentColor"/>`, "play": `<path d="M16.3944 12.0001L10 7.7371V16.263L16.3944 12.0001ZM19.376 12.4161L8.77735 19.4818C8.54759 19.635 8.23715 19.5729 8.08397 19.3432C8.02922 19.261 8 19.1645 8 19.0658V4.93433C8 4.65818 8.22386 4.43433 8.5 4.43433C8.59871 4.43433 8.69522 4.46355 8.77735 4.5183L19.376 11.584C19.6057 11.7372 19.6678 12.0477 19.5146 12.2774C19.478 12.3323 19.4309 12.3795 19.376 12.4161Z" fill="currentColor"/>`,
"play-list-add": `<path d="M2 18H12V20H2V18ZM2 11H22V13H2V11ZM2 4H22V6H2V4ZM18 18V15H20V18H23V20H20V23H18V20H15V18H18Z" fill="currentColor"/>`, "play-list-add": `<path d="M2 18H12V20H2V18ZM2 11H22V13H2V11ZM2 4H22V6H2V4ZM18 18V15H20V18H23V20H20V23H18V20H15V18H18Z" fill="currentColor"/>`,
"plug-2": `<path d="M13 18V20H19V22H13C11.8954 22 11 21.1046 11 20V18H8C5.79086 18 4 16.2091 4 14V7C4 6.44772 4.44772 6 5 6H7V2H9V6H15V2H17V6H19C19.5523 6 20 6.44772 20 7V14C20 16.2091 18.2091 18 16 18H13ZM8 16H16C17.1046 16 18 15.1046 18 14V11H6V14C6 15.1046 6.89543 16 8 16ZM18 8H6V9H18V8ZM12 14.5C11.4477 14.5 11 14.0523 11 13.5C11 12.9477 11.4477 12.5 12 12.5C12.5523 12.5 13 12.9477 13 13.5C13 14.0523 12.5523 14.5 12 14.5ZM11 2H13V5H11V2Z" fill="currentColor"/>`,
"plug": `<path d="M13 18V20H19V22H13C11.8954 22 11 21.1046 11 20V18H8C5.79086 18 4 16.2091 4 14V7C4 6.44772 4.44772 6 5 6H8V2H10V6H14V2H16V6H19C19.5523 6 20 6.44772 20 7V14C20 16.2091 18.2091 18 16 18H13ZM8 16H16C17.1046 16 18 15.1046 18 14V11H6V14C6 15.1046 6.89543 16 8 16ZM18 8H6V9H18V8ZM12 14.5C11.4477 14.5 11 14.0523 11 13.5C11 12.9477 11.4477 12.5 12 12.5C12.5523 12.5 13 12.9477 13 13.5C13 14.0523 12.5523 14.5 12 14.5Z" fill="currentColor"/>`, "plug": `<path d="M13 18V20H19V22H13C11.8954 22 11 21.1046 11 20V18H8C5.79086 18 4 16.2091 4 14V7C4 6.44772 4.44772 6 5 6H8V2H10V6H14V2H16V6H19C19.5523 6 20 6.44772 20 7V14C20 16.2091 18.2091 18 16 18H13ZM8 16H16C17.1046 16 18 15.1046 18 14V11H6V14C6 15.1046 6.89543 16 8 16ZM18 8H6V9H18V8ZM12 14.5C11.4477 14.5 11 14.0523 11 13.5C11 12.9477 11.4477 12.5 12 12.5C12.5523 12.5 13 12.9477 13 13.5C13 14.0523 12.5523 14.5 12 14.5Z" fill="currentColor"/>`,
"plug-2": `<path d="M13 18V20H19V22H13C11.8954 22 11 21.1046 11 20V18H8C5.79086 18 4 16.2091 4 14V7C4 6.44772 4.44772 6 5 6H7V2H9V6H15V2H17V6H19C19.5523 6 20 6.44772 20 7V14C20 16.2091 18.2091 18 16 18H13ZM8 16H16C17.1046 16 18 15.1046 18 14V11H6V14C6 15.1046 6.89543 16 8 16ZM18 8H6V9H18V8ZM12 14.5C11.4477 14.5 11 14.0523 11 13.5C11 12.9477 11.4477 12.5 12 12.5C12.5523 12.5 13 12.9477 13 13.5C13 14.0523 12.5523 14.5 12 14.5ZM11 2H13V5H11V2Z" fill="currentColor"/>`,
"pulse": `<path d="M9 7.53861L15 21.5386L18.6594 13H23V11H17.3406L15 16.4614L9 2.46143L5.3406 11H1V13H6.6594L9 7.53861Z" fill="currentColor"/>`, "pulse": `<path d="M9 7.53861L15 21.5386L18.6594 13H23V11H17.3406L15 16.4614L9 2.46143L5.3406 11H1V13H6.6594L9 7.53861Z" fill="currentColor"/>`,
"pushpin-2-fill": `<path d="M18 3V5H17V11L19 14V16H13V23H11V16H5V14L7 11V5H6V3H18Z" fill="currentColor"/>`,
"pushpin-2": `<path d="M18 3V5H17V11L19 14V16H13V23H11V16H5V14L7 11V5H6V3H18ZM9 5V11.6056L7.4037 14H16.5963L15 11.6056V5H9Z" fill="currentColor"/>`,
"pushpin": `<path d="M13.8273 1.69L22.3126 10.1753L20.8984 11.5895L20.1913 10.8824L15.9486 15.125L15.2415 18.6606L13.8273 20.0748L9.58466 15.8321L4.63492 20.7819L3.2207 19.3677L8.17045 14.4179L3.92781 10.1753L5.34202 8.76107L8.87756 8.05396L13.1202 3.81132L12.4131 3.10422L13.8273 1.69ZM14.5344 5.22554L9.86358 9.89637L7.0417 10.4607L13.5418 16.9609L14.1062 14.139L18.7771 9.46818L14.5344 5.22554Z" fill="currentColor"/>`, "pushpin": `<path d="M13.8273 1.69L22.3126 10.1753L20.8984 11.5895L20.1913 10.8824L15.9486 15.125L15.2415 18.6606L13.8273 20.0748L9.58466 15.8321L4.63492 20.7819L3.2207 19.3677L8.17045 14.4179L3.92781 10.1753L5.34202 8.76107L8.87756 8.05396L13.1202 3.81132L12.4131 3.10422L13.8273 1.69ZM14.5344 5.22554L9.86358 9.89637L7.0417 10.4607L13.5418 16.9609L14.1062 14.139L18.7771 9.46818L14.5344 5.22554Z" fill="currentColor"/>`,
"pushpin-2": `<path d="M18 3V5H17V11L19 14V16H13V23H11V16H5V14L7 11V5H6V3H18ZM9 5V11.6056L7.4037 14H16.5963L15 11.6056V5H9Z" fill="currentColor"/>`,
"pushpin-2-fill": `<path d="M18 3V5H17V11L19 14V16H13V23H11V16H5V14L7 11V5H6V3H18Z" fill="currentColor"/>`,
"question": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM13 13.3551V14H11V12.5C11 11.9477 11.4477 11.5 12 11.5C12.8284 11.5 13.5 10.8284 13.5 10C13.5 9.17157 12.8284 8.5 12 8.5C11.2723 8.5 10.6656 9.01823 10.5288 9.70577L8.56731 9.31346C8.88637 7.70919 10.302 6.5 12 6.5C13.933 6.5 15.5 8.067 15.5 10C15.5 11.5855 14.4457 12.9248 13 13.3551Z" fill="currentColor"/>`, "question": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM13 13.3551V14H11V12.5C11 11.9477 11.4477 11.5 12 11.5C12.8284 11.5 13.5 10.8284 13.5 10C13.5 9.17157 12.8284 8.5 12 8.5C11.2723 8.5 10.6656 9.01823 10.5288 9.70577L8.56731 9.31346C8.88637 7.70919 10.302 6.5 12 6.5C13.933 6.5 15.5 8.067 15.5 10C15.5 11.5855 14.4457 12.9248 13 13.3551Z" fill="currentColor"/>`,
"record-circle": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z" fill="currentColor"/>`, "record-circle": `<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z" fill="currentColor"/>`,
"refresh": `<path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="currentColor"/>`, "refresh": `<path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="currentColor"/>`,
"restart": `<path d="M18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="currentColor"/>`, "restart": `<path d="M18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="currentColor"/>`,
"robot-2": `<path d="M13.5 2C13.5 2.44425 13.3069 2.84339 13 3.11805V5H18C19.6569 5 21 6.34315 21 8V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V8C3 6.34315 4.34315 5 6 5H11V3.11805C10.6931 2.84339 10.5 2.44425 10.5 2C10.5 1.17157 11.1716 0.5 12 0.5C12.8284 0.5 13.5 1.17157 13.5 2ZM6 7C5.44772 7 5 7.44772 5 8V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V8C19 7.44772 18.5523 7 18 7H13H11H6ZM2 10H0V16H2V10ZM22 10H24V16H22V10ZM9 14.5C9.82843 14.5 10.5 13.8284 10.5 13C10.5 12.1716 9.82843 11.5 9 11.5C8.17157 11.5 7.5 12.1716 7.5 13C7.5 13.8284 8.17157 14.5 9 14.5ZM15 14.5C15.8284 14.5 16.5 13.8284 16.5 13C16.5 12.1716 15.8284 11.5 15 11.5C14.1716 11.5 13.5 12.1716 13.5 13C13.5 13.8284 14.1716 14.5 15 14.5Z" fill="currentColor"/>`,
"robot": `<path d="M13 4.05493C17.5 4.55237 21 8.36745 21 13V22H3V13C3 8.36745 6.50005 4.55237 11 4.05493V1H13V4.05493ZM19 20V13C19 9.13401 15.866 6 12 6C8.13401 6 5 9.13401 5 13V20H19ZM12 18C9.23858 18 7 15.7614 7 13C7 10.2386 9.23858 8 12 8C14.7614 8 17 10.2386 17 13C17 15.7614 14.7614 18 12 18ZM12 16C13.6569 16 15 14.6569 15 13C15 11.3431 13.6569 10 12 10C10.3431 10 9 11.3431 9 13C9 14.6569 10.3431 16 12 16ZM12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13C13 13.5523 12.5523 14 12 14Z" fill="currentColor"/>`, "robot": `<path d="M13 4.05493C17.5 4.55237 21 8.36745 21 13V22H3V13C3 8.36745 6.50005 4.55237 11 4.05493V1H13V4.05493ZM19 20V13C19 9.13401 15.866 6 12 6C8.13401 6 5 9.13401 5 13V20H19ZM12 18C9.23858 18 7 15.7614 7 13C7 10.2386 9.23858 8 12 8C14.7614 8 17 10.2386 17 13C17 15.7614 14.7614 18 12 18ZM12 16C13.6569 16 15 14.6569 15 13C15 11.3431 13.6569 10 12 10C10.3431 10 9 11.3431 9 13C9 14.6569 10.3431 16 12 16ZM12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13C13 13.5523 12.5523 14 12 14Z" fill="currentColor"/>`,
"robot-2": `<path d="M13.5 2C13.5 2.44425 13.3069 2.84339 13 3.11805V5H18C19.6569 5 21 6.34315 21 8V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V8C3 6.34315 4.34315 5 6 5H11V3.11805C10.6931 2.84339 10.5 2.44425 10.5 2C10.5 1.17157 11.1716 0.5 12 0.5C12.8284 0.5 13.5 1.17157 13.5 2ZM6 7C5.44772 7 5 7.44772 5 8V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V8C19 7.44772 18.5523 7 18 7H13H11H6ZM2 10H0V16H2V10ZM22 10H24V16H22V10ZM9 14.5C9.82843 14.5 10.5 13.8284 10.5 13C10.5 12.1716 9.82843 11.5 9 11.5C8.17157 11.5 7.5 12.1716 7.5 13C7.5 13.8284 8.17157 14.5 9 14.5ZM15 14.5C15.8284 14.5 16.5 13.8284 16.5 13C16.5 12.1716 15.8284 11.5 15 11.5C14.1716 11.5 13.5 12.1716 13.5 13C13.5 13.8284 14.1716 14.5 15 14.5Z" fill="currentColor"/>`,
"rocket": `<path d="M4.99958 12.9999C4.99958 7.91198 7.90222 3.5636 11.9996 1.81799C16.0969 3.5636 18.9996 7.91198 18.9996 12.9999C18.9996 13.8229 18.9236 14.6264 18.779 15.4027L20.7194 17.2353C20.8845 17.3913 20.9238 17.6389 20.815 17.8383L18.3196 22.4133C18.1873 22.6557 17.8836 22.7451 17.6412 22.6128C17.5993 22.59 17.5608 22.5612 17.5271 22.5274L15.2925 20.2928C15.1049 20.1053 14.8506 19.9999 14.5854 19.9999H9.41379C9.14857 19.9999 8.89422 20.1053 8.70668 20.2928L6.47209 22.5274C6.27683 22.7227 5.96025 22.7227 5.76498 22.5274C5.73122 22.4937 5.70246 22.4552 5.67959 22.4133L3.18412 17.8383C3.07537 17.6389 3.11464 17.3913 3.27975 17.2353L5.22014 15.4027C5.07551 14.6264 4.99958 13.8229 4.99958 12.9999ZM6.47542 19.6957L7.29247 18.8786C7.85508 18.316 8.61814 17.9999 9.41379 17.9999H14.5854C15.381 17.9999 16.1441 18.316 16.7067 18.8786L17.5237 19.6957L18.5056 17.8955L17.4058 16.8568C16.9117 16.3901 16.6884 15.7045 16.8128 15.0364C16.9366 14.3722 16.9996 13.6911 16.9996 12.9999C16.9996 9.13037 15.0045 5.69965 11.9996 4.04033C8.99462 5.69965 6.99958 9.13037 6.99958 12.9999C6.99958 13.6911 7.06255 14.3722 7.18631 15.0364C7.31078 15.7045 7.08746 16.3901 6.59338 16.8568L5.49353 17.8955L6.47542 19.6957ZM11.9996 12.9999C10.895 12.9999 9.99958 12.1045 9.99958 10.9999C9.99958 9.89537 10.895 8.99994 11.9996 8.99994C13.1041 8.99994 13.9996 9.89537 13.9996 10.9999C13.9996 12.1045 13.1041 12.9999 11.9996 12.9999Z" fill="currentColor"/>`, "rocket": `<path d="M4.99958 12.9999C4.99958 7.91198 7.90222 3.5636 11.9996 1.81799C16.0969 3.5636 18.9996 7.91198 18.9996 12.9999C18.9996 13.8229 18.9236 14.6264 18.779 15.4027L20.7194 17.2353C20.8845 17.3913 20.9238 17.6389 20.815 17.8383L18.3196 22.4133C18.1873 22.6557 17.8836 22.7451 17.6412 22.6128C17.5993 22.59 17.5608 22.5612 17.5271 22.5274L15.2925 20.2928C15.1049 20.1053 14.8506 19.9999 14.5854 19.9999H9.41379C9.14857 19.9999 8.89422 20.1053 8.70668 20.2928L6.47209 22.5274C6.27683 22.7227 5.96025 22.7227 5.76498 22.5274C5.73122 22.4937 5.70246 22.4552 5.67959 22.4133L3.18412 17.8383C3.07537 17.6389 3.11464 17.3913 3.27975 17.2353L5.22014 15.4027C5.07551 14.6264 4.99958 13.8229 4.99958 12.9999ZM6.47542 19.6957L7.29247 18.8786C7.85508 18.316 8.61814 17.9999 9.41379 17.9999H14.5854C15.381 17.9999 16.1441 18.316 16.7067 18.8786L17.5237 19.6957L18.5056 17.8955L17.4058 16.8568C16.9117 16.3901 16.6884 15.7045 16.8128 15.0364C16.9366 14.3722 16.9996 13.6911 16.9996 12.9999C16.9996 9.13037 15.0045 5.69965 11.9996 4.04033C8.99462 5.69965 6.99958 9.13037 6.99958 12.9999C6.99958 13.6911 7.06255 14.3722 7.18631 15.0364C7.31078 15.7045 7.08746 16.3901 6.59338 16.8568L5.49353 17.8955L6.47542 19.6957ZM11.9996 12.9999C10.895 12.9999 9.99958 12.1045 9.99958 10.9999C9.99958 9.89537 10.895 8.99994 11.9996 8.99994C13.1041 8.99994 13.9996 9.89537 13.9996 10.9999C13.9996 12.1045 13.1041 12.9999 11.9996 12.9999Z" fill="currentColor"/>`,
"save-3": `<path d="M18 19H19V6.82843L17.1716 5H16V9H7V5H5V19H6V12H18V19ZM4 3H18L20.7071 5.70711C20.8946 5.89464 21 6.149 21 6.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM8 14V19H16V14H8Z" fill="currentColor"/>`, "save-3": `<path d="M18 19H19V6.82843L17.1716 5H16V9H7V5H5V19H6V12H18V19ZM4 3H18L20.7071 5.70711C20.8946 5.89464 21 6.149 21 6.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM8 14V19H16V14H8Z" fill="currentColor"/>`,
"scales-3": `<path d="M12.9985 2L12.9979 3.278L17.9985 4.94591L21.631 3.73509L22.2634 5.63246L19.2319 6.643L22.3272 15.1549C21.2353 16.2921 19.6996 17 17.9985 17C16.2975 17 14.7618 16.2921 13.6699 15.1549L16.7639 6.643L12.9979 5.387V19H16.9985V21H6.99854V19H10.9979V5.387L7.23192 6.643L10.3272 15.1549C9.23528 16.2921 7.69957 17 5.99854 17C4.2975 17 2.76179 16.2921 1.66992 15.1549L4.76392 6.643L1.73363 5.63246L2.36608 3.73509L5.99854 4.94591L10.9979 3.278L10.9985 2H12.9985ZM17.9985 9.10267L16.04 14.4892C16.628 14.8201 17.2979 15 17.9985 15C18.6992 15 19.3691 14.8201 19.957 14.4892L17.9985 9.10267ZM5.99854 9.10267L4.04004 14.4892C4.62795 14.8201 5.29792 15 5.99854 15C6.69916 15 7.36912 14.8201 7.95703 14.4892L5.99854 9.10267Z" fill="currentColor"/>`, "scales-3": `<path d="M12.9985 2L12.9979 3.278L17.9985 4.94591L21.631 3.73509L22.2634 5.63246L19.2319 6.643L22.3272 15.1549C21.2353 16.2921 19.6996 17 17.9985 17C16.2975 17 14.7618 16.2921 13.6699 15.1549L16.7639 6.643L12.9979 5.387V19H16.9985V21H6.99854V19H10.9979V5.387L7.23192 6.643L10.3272 15.1549C9.23528 16.2921 7.69957 17 5.99854 17C4.2975 17 2.76179 16.2921 1.66992 15.1549L4.76392 6.643L1.73363 5.63246L2.36608 3.73509L5.99854 4.94591L10.9979 3.278L10.9985 2H12.9985ZM17.9985 9.10267L16.04 14.4892C16.628 14.8201 17.2979 15 17.9985 15C18.6992 15 19.3691 14.8201 19.957 14.4892L17.9985 9.10267ZM5.99854 9.10267L4.04004 14.4892C4.62795 14.8201 5.29792 15 5.99854 15C6.69916 15 7.36912 14.8201 7.95703 14.4892L5.99854 9.10267Z" fill="currentColor"/>`,
"scan-2": `<path d="M5.67127 4.25705L13.4142 12L12 13.4142L8.55382 9.96803C8.20193 10.5635 8 11.2582 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.87494 14.3429 8.13693 12.2503 8.00771L10.4459 6.20323C10.9416 6.07067 11.4625 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12C6 10.7042 6.41079 9.50428 7.10925 8.52347L5.68014 7.09436C4.62708 8.44904 4 10.1513 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C10.8915 4 9.83557 4.22547 8.8757 4.63306L7.37443 3.13179C8.75768 2.40883 10.3311 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 8.87842 3.43029 6.09091 5.67127 4.25705Z" fill="currentColor"/>`, "scan-2": `<path d="M5.67127 4.25705L13.4142 12L12 13.4142L8.55382 9.96803C8.20193 10.5635 8 11.2582 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.87494 14.3429 8.13693 12.2503 8.00771L10.4459 6.20323C10.9416 6.07067 11.4625 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12C6 10.7042 6.41079 9.50428 7.10925 8.52347L5.68014 7.09436C4.62708 8.44904 4 10.1513 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C10.8915 4 9.83557 4.22547 8.8757 4.63306L7.37443 3.13179C8.75768 2.40883 10.3311 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 8.87842 3.43029 6.09091 5.67127 4.25705Z" fill="currentColor"/>`,
"scissors": `<path d="M9.44618 8.02867L12 10.5825L18.7279 3.85457C19.509 3.07352 20.7753 3.07352 21.5563 3.85457L9.44618 15.9647C9.79807 16.5603 10 17.2549 10 17.9967C10 20.2058 8.20914 21.9967 6 21.9967C3.79086 21.9967 2 20.2058 2 17.9967C2 15.7876 3.79086 13.9967 6 13.9967C6.74181 13.9967 7.43645 14.1986 8.03197 14.5505L10.5858 11.9967L8.03197 9.44289C7.43645 9.79478 6.74181 9.9967 6 9.9967C3.79086 9.9967 2 8.20584 2 5.9967C2 3.78756 3.79086 1.9967 6 1.9967C8.20914 1.9967 10 3.78756 10 5.9967C10 6.73851 9.79807 7.43316 9.44618 8.02867ZM14.8255 13.408L21.5563 20.1388C20.7753 20.9199 19.509 20.9199 18.7279 20.1388L13.4113 14.8222L14.8255 13.408ZM7.41421 16.5825C7.05228 16.2206 6.55228 15.9967 6 15.9967C4.89543 15.9967 4 16.8921 4 17.9967C4 19.1013 4.89543 19.9967 6 19.9967C7.10457 19.9967 8 19.1013 8 17.9967C8 17.4444 7.77614 16.9444 7.41421 16.5825ZM7.41421 7.41092C7.77614 7.04899 8 6.54899 8 5.9967C8 4.89213 7.10457 3.9967 6 3.9967C4.89543 3.9967 4 4.89213 4 5.9967C4 7.10127 4.89543 7.9967 6 7.9967C6.55228 7.9967 7.05228 7.77285 7.41421 7.41092Z" fill="currentColor"/>`, "scissors": `<path d="M9.44618 8.02867L12 10.5825L18.7279 3.85457C19.509 3.07352 20.7753 3.07352 21.5563 3.85457L9.44618 15.9647C9.79807 16.5603 10 17.2549 10 17.9967C10 20.2058 8.20914 21.9967 6 21.9967C3.79086 21.9967 2 20.2058 2 17.9967C2 15.7876 3.79086 13.9967 6 13.9967C6.74181 13.9967 7.43645 14.1986 8.03197 14.5505L10.5858 11.9967L8.03197 9.44289C7.43645 9.79478 6.74181 9.9967 6 9.9967C3.79086 9.9967 2 8.20584 2 5.9967C2 3.78756 3.79086 1.9967 6 1.9967C8.20914 1.9967 10 3.78756 10 5.9967C10 6.73851 9.79807 7.43316 9.44618 8.02867ZM14.8255 13.408L21.5563 20.1388C20.7753 20.9199 19.509 20.9199 18.7279 20.1388L13.4113 14.8222L14.8255 13.408ZM7.41421 16.5825C7.05228 16.2206 6.55228 15.9967 6 15.9967C4.89543 15.9967 4 16.8921 4 17.9967C4 19.1013 4.89543 19.9967 6 19.9967C7.10457 19.9967 8 19.1013 8 17.9967C8 17.4444 7.77614 16.9444 7.41421 16.5825ZM7.41421 7.41092C7.77614 7.04899 8 6.54899 8 5.9967C8 4.89213 7.10457 3.9967 6 3.9967C4.89543 3.9967 4 4.89213 4 5.9967C4 7.10127 4.89543 7.9967 6 7.9967C6.55228 7.9967 7.05228 7.77285 7.41421 7.41092Z" fill="currentColor"/>`,
"search-eye": `<path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748ZM12.1779 7.17624C11.4834 7.48982 11 8.18846 11 9C11 10.1046 11.8954 11 13 11C13.8115 11 14.5102 10.5166 14.8238 9.82212C14.9383 10.1945 15 10.59 15 11C15 13.2091 13.2091 15 11 15C8.79086 15 7 13.2091 7 11C7 8.79086 8.79086 7 11 7C11.41 7 11.8055 7.06167 12.1779 7.17624Z" fill="currentColor"/>`,
"search": `<path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z" fill="currentColor"/>`, "search": `<path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z" fill="currentColor"/>`,
"send-plane-2": `<path d="M3.5 1.34558C3.58425 1.34558 3.66714 1.36687 3.74096 1.40747L22.2034 11.5618C22.4454 11.6949 22.5337 11.9989 22.4006 12.2409C22.3549 12.324 22.2865 12.3924 22.2034 12.4381L3.74096 22.5924C3.499 22.7255 3.19497 22.6372 3.06189 22.3953C3.02129 22.3214 3 22.2386 3 22.1543V1.84558C3 1.56944 3.22386 1.34558 3.5 1.34558ZM5 4.38249V10.9999H10V12.9999H5V19.6174L18.8499 11.9999L5 4.38249Z" fill="currentColor"/>`, "search-eye": `<path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748ZM12.1779 7.17624C11.4834 7.48982 11 8.18846 11 9C11 10.1046 11.8954 11 13 11C13.8115 11 14.5102 10.5166 14.8238 9.82212C14.9383 10.1945 15 10.59 15 11C15 13.2091 13.2091 15 11 15C8.79086 15 7 13.2091 7 11C7 8.79086 8.79086 7 11 7C11.41 7 11.8055 7.06167 12.1779 7.17624Z" fill="currentColor"/>`,
"send-plane": `<path d="M21.7267 2.95694L16.2734 22.0432C16.1225 22.5716 15.7979 22.5956 15.5563 22.1126L11 13L1.9229 9.36919C1.41322 9.16532 1.41953 8.86022 1.95695 8.68108L21.0432 2.31901C21.5716 2.14285 21.8747 2.43866 21.7267 2.95694ZM19.0353 5.09647L6.81221 9.17085L12.4488 11.4255L15.4895 17.5068L19.0353 5.09647Z" fill="currentColor"/>`, "send-plane": `<path d="M21.7267 2.95694L16.2734 22.0432C16.1225 22.5716 15.7979 22.5956 15.5563 22.1126L11 13L1.9229 9.36919C1.41322 9.16532 1.41953 8.86022 1.95695 8.68108L21.0432 2.31901C21.5716 2.14285 21.8747 2.43866 21.7267 2.95694ZM19.0353 5.09647L6.81221 9.17085L12.4488 11.4255L15.4895 17.5068L19.0353 5.09647Z" fill="currentColor"/>`,
"send-plane-2": `<path d="M3.5 1.34558C3.58425 1.34558 3.66714 1.36687 3.74096 1.40747L22.2034 11.5618C22.4454 11.6949 22.5337 11.9989 22.4006 12.2409C22.3549 12.324 22.2865 12.3924 22.2034 12.4381L3.74096 22.5924C3.499 22.7255 3.19497 22.6372 3.06189 22.3953C3.02129 22.3214 3 22.2386 3 22.1543V1.84558C3 1.56944 3.22386 1.34558 3.5 1.34558ZM5 4.38249V10.9999H10V12.9999H5V19.6174L18.8499 11.9999L5 4.38249Z" fill="currentColor"/>`,
"server": `<path d="M5 11H19V5H5V11ZM21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20C20.5523 3 21 3.44772 21 4ZM19 13H5V19H19V13ZM7 15H10V17H7V15ZM7 7H10V9H7V7Z" fill="currentColor"/>`, "server": `<path d="M5 11H19V5H5V11ZM21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20C20.5523 3 21 3.44772 21 4ZM19 13H5V19H19V13ZM7 15H10V17H7V15ZM7 7H10V9H7V7Z" fill="currentColor"/>`,
"settings-3": `<path d="M3.33946 17.0002C2.90721 16.2515 2.58277 15.4702 2.36133 14.6741C3.3338 14.1779 3.99972 13.1668 3.99972 12.0002C3.99972 10.8345 3.3348 9.824 2.36353 9.32741C2.81025 7.71651 3.65857 6.21627 4.86474 4.99001C5.7807 5.58416 6.98935 5.65534 7.99972 5.072C9.01009 4.48866 9.55277 3.40635 9.4962 2.31604C11.1613 1.8846 12.8847 1.90004 14.5031 2.31862C14.4475 3.40806 14.9901 4.48912 15.9997 5.072C17.0101 5.65532 18.2187 5.58416 19.1346 4.99007C19.7133 5.57986 20.2277 6.25151 20.66 7.00021C21.0922 7.7489 21.4167 8.53025 21.6381 9.32628C20.6656 9.82247 19.9997 10.8336 19.9997 12.0002C19.9997 13.166 20.6646 14.1764 21.6359 14.673C21.1892 16.2839 20.3409 17.7841 19.1347 19.0104C18.2187 18.4163 17.0101 18.3451 15.9997 18.9284C14.9893 19.5117 14.4467 20.5941 14.5032 21.6844C12.8382 22.1158 11.1148 22.1004 9.49633 21.6818C9.55191 20.5923 9.00929 19.5113 7.99972 18.9284C6.98938 18.3451 5.78079 18.4162 4.86484 19.0103C4.28617 18.4205 3.77172 17.7489 3.33946 17.0002ZM8.99972 17.1964C10.0911 17.8265 10.8749 18.8227 11.2503 19.9659C11.7486 20.0133 12.2502 20.014 12.7486 19.9675C13.1238 18.8237 13.9078 17.8268 14.9997 17.1964C16.0916 16.5659 17.347 16.3855 18.5252 16.6324C18.8146 16.224 19.0648 15.7892 19.2729 15.334C18.4706 14.4373 17.9997 13.2604 17.9997 12.0002C17.9997 10.74 18.4706 9.5632 19.2729 8.6665C19.1688 8.4405 19.0538 8.21822 18.9279 8.00021C18.802 7.78219 18.667 7.57148 18.5233 7.36842C17.3457 7.61476 16.0911 7.43414 14.9997 6.80405C13.9083 6.17395 13.1246 5.17768 12.7491 4.03455C12.2509 3.98714 11.7492 3.98646 11.2509 4.03292C10.8756 5.17671 10.0916 6.17364 8.99972 6.80405C7.9078 7.43447 6.65245 7.61494 5.47428 7.36803C5.18485 7.77641 4.93463 8.21117 4.72656 8.66637C5.52881 9.56311 5.99972 10.74 5.99972 12.0002C5.99972 13.2604 5.52883 14.4372 4.72656 15.3339C4.83067 15.5599 4.94564 15.7822 5.07152 16.0002C5.19739 16.2182 5.3324 16.4289 5.47612 16.632C6.65377 16.3857 7.90838 16.5663 8.99972 17.1964ZM11.9997 15.0002C10.3429 15.0002 8.99972 13.6571 8.99972 12.0002C8.99972 10.3434 10.3429 9.00021 11.9997 9.00021C13.6566 9.00021 14.9997 10.3434 14.9997 12.0002C14.9997 13.6571 13.6566 15.0002 11.9997 15.0002ZM11.9997 13.0002C12.552 13.0002 12.9997 12.5525 12.9997 12.0002C12.9997 11.4479 12.552 11.0002 11.9997 11.0002C11.4474 11.0002 10.9997 11.4479 10.9997 12.0002C10.9997 12.5525 11.4474 13.0002 11.9997 13.0002Z" fill="currentColor"/>`, "settings-3": `<path d="M3.33946 17.0002C2.90721 16.2515 2.58277 15.4702 2.36133 14.6741C3.3338 14.1779 3.99972 13.1668 3.99972 12.0002C3.99972 10.8345 3.3348 9.824 2.36353 9.32741C2.81025 7.71651 3.65857 6.21627 4.86474 4.99001C5.7807 5.58416 6.98935 5.65534 7.99972 5.072C9.01009 4.48866 9.55277 3.40635 9.4962 2.31604C11.1613 1.8846 12.8847 1.90004 14.5031 2.31862C14.4475 3.40806 14.9901 4.48912 15.9997 5.072C17.0101 5.65532 18.2187 5.58416 19.1346 4.99007C19.7133 5.57986 20.2277 6.25151 20.66 7.00021C21.0922 7.7489 21.4167 8.53025 21.6381 9.32628C20.6656 9.82247 19.9997 10.8336 19.9997 12.0002C19.9997 13.166 20.6646 14.1764 21.6359 14.673C21.1892 16.2839 20.3409 17.7841 19.1347 19.0104C18.2187 18.4163 17.0101 18.3451 15.9997 18.9284C14.9893 19.5117 14.4467 20.5941 14.5032 21.6844C12.8382 22.1158 11.1148 22.1004 9.49633 21.6818C9.55191 20.5923 9.00929 19.5113 7.99972 18.9284C6.98938 18.3451 5.78079 18.4162 4.86484 19.0103C4.28617 18.4205 3.77172 17.7489 3.33946 17.0002ZM8.99972 17.1964C10.0911 17.8265 10.8749 18.8227 11.2503 19.9659C11.7486 20.0133 12.2502 20.014 12.7486 19.9675C13.1238 18.8237 13.9078 17.8268 14.9997 17.1964C16.0916 16.5659 17.347 16.3855 18.5252 16.6324C18.8146 16.224 19.0648 15.7892 19.2729 15.334C18.4706 14.4373 17.9997 13.2604 17.9997 12.0002C17.9997 10.74 18.4706 9.5632 19.2729 8.6665C19.1688 8.4405 19.0538 8.21822 18.9279 8.00021C18.802 7.78219 18.667 7.57148 18.5233 7.36842C17.3457 7.61476 16.0911 7.43414 14.9997 6.80405C13.9083 6.17395 13.1246 5.17768 12.7491 4.03455C12.2509 3.98714 11.7492 3.98646 11.2509 4.03292C10.8756 5.17671 10.0916 6.17364 8.99972 6.80405C7.9078 7.43447 6.65245 7.61494 5.47428 7.36803C5.18485 7.77641 4.93463 8.21117 4.72656 8.66637C5.52881 9.56311 5.99972 10.74 5.99972 12.0002C5.99972 13.2604 5.52883 14.4372 4.72656 15.3339C4.83067 15.5599 4.94564 15.7822 5.07152 16.0002C5.19739 16.2182 5.3324 16.4289 5.47612 16.632C6.65377 16.3857 7.90838 16.5663 8.99972 17.1964ZM11.9997 15.0002C10.3429 15.0002 8.99972 13.6571 8.99972 12.0002C8.99972 10.3434 10.3429 9.00021 11.9997 9.00021C13.6566 9.00021 14.9997 10.3434 14.9997 12.0002C14.9997 13.6571 13.6566 15.0002 11.9997 15.0002ZM11.9997 13.0002C12.552 13.0002 12.9997 12.5525 12.9997 12.0002C12.9997 11.4479 12.552 11.0002 11.9997 11.0002C11.4474 11.0002 10.9997 11.4479 10.9997 12.0002C10.9997 12.5525 11.4474 13.0002 11.9997 13.0002Z" fill="currentColor"/>`,
"share-2": `<path d="M12 2.58582L18.2071 8.79292L16.7929 10.2071L13 6.41424V16H11V6.41424L7.20711 10.2071L5.79289 8.79292L12 2.58582ZM3 18V14H5V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V14H21V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18Z" fill="currentColor"/>`, "share-2": `<path d="M12 2.58582L18.2071 8.79292L16.7929 10.2071L13 6.41424V16H11V6.41424L7.20711 10.2071L5.79289 8.79292L12 2.58582ZM3 18V14H5V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V14H21V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18Z" fill="currentColor"/>`,
"shield": `<path d="M3.78307 2.82598L12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598ZM5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879L5 4.60434Z" fill="currentColor"/>`,
"shield-check": `<path d="M12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598L12 1ZM12 3.04879L5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879ZM16.4524 8.22183L17.8666 9.63604L11.5026 16L7.25999 11.7574L8.67421 10.3431L11.5019 13.1709L16.4524 8.22183Z" fill="currentColor"/>`, "shield-check": `<path d="M12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598L12 1ZM12 3.04879L5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879ZM16.4524 8.22183L17.8666 9.63604L11.5026 16L7.25999 11.7574L8.67421 10.3431L11.5019 13.1709L16.4524 8.22183Z" fill="currentColor"/>`,
"shield-keyhole": `<path d="M12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598L12 1ZM12 3.04879L5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879ZM12 7C13.1046 7 14 7.89543 14 9C14 9.73984 13.5983 10.3858 13.0011 10.7318L13 15H11L10.9999 10.7324C10.4022 10.3866 10 9.74025 10 9C10 7.89543 10.8954 7 12 7Z" fill="currentColor"/>`, "shield-keyhole": `<path d="M12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598L12 1ZM12 3.04879L5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879ZM12 7C13.1046 7 14 7.89543 14 9C14 9.73984 13.5983 10.3858 13.0011 10.7318L13 15H11L10.9999 10.7324C10.4022 10.3866 10 9.74025 10 9C10 7.89543 10.8954 7 12 7Z" fill="currentColor"/>`,
"shield": `<path d="M3.78307 2.82598L12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598ZM5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879L5 4.60434Z" fill="currentColor"/>`,
"shield-user": `<path d="M3.78307 2.82598L12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598ZM5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879L5 4.60434ZM12 11C10.6193 11 9.5 9.88071 9.5 8.5C9.5 7.11929 10.6193 6 12 6C13.3807 6 14.5 7.11929 14.5 8.5C14.5 9.88071 13.3807 11 12 11ZM7.52746 16C7.77619 13.75 9.68372 12 12 12C14.3163 12 16.2238 13.75 16.4725 16H7.52746Z" fill="currentColor"/>`, "shield-user": `<path d="M3.78307 2.82598L12 1L20.2169 2.82598C20.6745 2.92766 21 3.33347 21 3.80217V13.7889C21 15.795 19.9974 17.6684 18.3282 18.7812L12 23L5.6718 18.7812C4.00261 17.6684 3 15.795 3 13.7889V3.80217C3 3.33347 3.32553 2.92766 3.78307 2.82598ZM5 4.60434V13.7889C5 15.1263 5.6684 16.3752 6.7812 17.1171L12 20.5963L17.2188 17.1171C18.3316 16.3752 19 15.1263 19 13.7889V4.60434L12 3.04879L5 4.60434ZM12 11C10.6193 11 9.5 9.88071 9.5 8.5C9.5 7.11929 10.6193 6 12 6C13.3807 6 14.5 7.11929 14.5 8.5C14.5 9.88071 13.3807 11 12 11ZM7.52746 16C7.77619 13.75 9.68372 12 12 12C14.3163 12 16.2238 13.75 16.4725 16H7.52746Z" fill="currentColor"/>`,
"shuffle": `<path d="M18 17.8832V16L23 19L18 22V19.9095C14.9224 19.4698 12.2513 17.4584 11.0029 14.5453L11 14.5386L10.9971 14.5453C9.57893 17.8544 6.32508 20 2.72483 20H2V18H2.72483C5.52503 18 8.05579 16.3312 9.15885 13.7574L9.91203 12L9.15885 10.2426C8.05579 7.66878 5.52503 6 2.72483 6H2V4H2.72483C6.32508 4 9.57893 6.14557 10.9971 9.45473L11 9.46141L11.0029 9.45473C12.2513 6.5416 14.9224 4.53022 18 4.09051V2L23 5L18 8V6.11684C15.7266 6.53763 13.7737 8.0667 12.8412 10.2426L12.088 12L12.8412 13.7574C13.7737 15.9333 15.7266 17.4624 18 17.8832Z" fill="currentColor"/>`, "shuffle": `<path d="M18 17.8832V16L23 19L18 22V19.9095C14.9224 19.4698 12.2513 17.4584 11.0029 14.5453L11 14.5386L10.9971 14.5453C9.57893 17.8544 6.32508 20 2.72483 20H2V18H2.72483C5.52503 18 8.05579 16.3312 9.15885 13.7574L9.91203 12L9.15885 10.2426C8.05579 7.66878 5.52503 6 2.72483 6H2V4H2.72483C6.32508 4 9.57893 6.14557 10.9971 9.45473L11 9.46141L11.0029 9.45473C12.2513 6.5416 14.9224 4.53022 18 4.09051V2L23 5L18 8V6.11684C15.7266 6.53763 13.7737 8.0667 12.8412 10.2426L12.088 12L12.8412 13.7574C13.7737 15.9333 15.7266 17.4624 18 17.8832Z" fill="currentColor"/>`,
"slash-commands-2": `<path d="M5 2C3.34315 2 2 3.34315 2 5V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V5C22 3.34315 20.6569 2 19 2H5ZM4 5C4 4.44772 4.44772 4 5 4H19C19.5523 4 20 4.44772 20 5V19C20 19.5523 19.5523 20 19 20H5C4.44772 20 4 19.5523 4 19V5ZM9.72318 18L16.5803 6H14.2768L7.41968 18H9.72318Z" fill="currentColor"/>`, "slash-commands-2": `<path d="M5 2C3.34315 2 2 3.34315 2 5V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V5C22 3.34315 20.6569 2 19 2H5ZM4 5C4 4.44772 4.44772 4 5 4H19C19.5523 4 20 4.44772 20 5V19C20 19.5523 19.5523 20 19 20H5C4.44772 20 4 19.5523 4 19V5ZM9.72318 18L16.5803 6H14.2768L7.41968 18H9.72318Z" fill="currentColor"/>`,
@@ -211,17 +212,17 @@ export const iconSpriteData = {
"sparkling": `<path d="M14 4.4375C15.3462 4.4375 16.4375 3.34619 16.4375 2H17.5625C17.5625 3.34619 18.6538 4.4375 20 4.4375V5.5625C18.6538 5.5625 17.5625 6.65381 17.5625 8H16.4375C16.4375 6.65381 15.3462 5.5625 14 5.5625V4.4375ZM1 11C4.31371 11 7 8.31371 7 5H9C9 8.31371 11.6863 11 15 11V13C11.6863 13 9 15.6863 9 19H7C7 15.6863 4.31371 13 1 13V11ZM4.87601 12C6.18717 12.7276 7.27243 13.8128 8 15.124 8.72757 13.8128 9.81283 12.7276 11.124 12 9.81283 11.2724 8.72757 10.1872 8 8.87601 7.27243 10.1872 6.18717 11.2724 4.87601 12ZM17.25 14C17.25 15.7949 15.7949 17.25 14 17.25V18.75C15.7949 18.75 17.25 20.2051 17.25 22H18.75C18.75 20.2051 20.2051 18.75 22 18.75V17.25C20.2051 17.25 18.75 15.7949 18.75 14H17.25Z" fill="currentColor"/>`, "sparkling": `<path d="M14 4.4375C15.3462 4.4375 16.4375 3.34619 16.4375 2H17.5625C17.5625 3.34619 18.6538 4.4375 20 4.4375V5.5625C18.6538 5.5625 17.5625 6.65381 17.5625 8H16.4375C16.4375 6.65381 15.3462 5.5625 14 5.5625V4.4375ZM1 11C4.31371 11 7 8.31371 7 5H9C9 8.31371 11.6863 11 15 11V13C11.6863 13 9 15.6863 9 19H7C7 15.6863 4.31371 13 1 13V11ZM4.87601 12C6.18717 12.7276 7.27243 13.8128 8 15.124 8.72757 13.8128 9.81283 12.7276 11.124 12 9.81283 11.2724 8.72757 10.1872 8 8.87601 7.27243 10.1872 6.18717 11.2724 4.87601 12ZM17.25 14C17.25 15.7949 15.7949 17.25 14 17.25V18.75C15.7949 18.75 17.25 20.2051 17.25 22H18.75C18.75 20.2051 20.2051 18.75 22 18.75V17.25C20.2051 17.25 18.75 15.7949 18.75 14H17.25Z" fill="currentColor"/>`,
"split-cells-horizontal": `<path d="M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20ZM11 5H5V19H11V15H13V19H19V5H13V9H11V5ZM15 9L18 12L15 15V13H9V15L6 12L9 9V11H15V9Z" fill="currentColor"/>`, "split-cells-horizontal": `<path d="M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20ZM11 5H5V19H11V15H13V19H19V5H13V9H11V5ZM15 9L18 12L15 15V13H9V15L6 12L9 9V11H15V9Z" fill="currentColor"/>`,
"stack": `<path d="M20.0833 15.1999L21.2854 15.9212C21.5221 16.0633 21.5989 16.3704 21.4569 16.6072C21.4146 16.6776 21.3557 16.7365 21.2854 16.7787L12.5144 22.0412C12.1977 22.2313 11.8021 22.2313 11.4854 22.0412L2.71451 16.7787C2.47772 16.6366 2.40093 16.3295 2.54301 16.0927C2.58523 16.0223 2.64413 15.9634 2.71451 15.9212L3.9166 15.1999L11.9999 20.0499L20.0833 15.1999ZM20.0833 10.4999L21.2854 11.2212C21.5221 11.3633 21.5989 11.6704 21.4569 11.9072C21.4146 11.9776 21.3557 12.0365 21.2854 12.0787L11.9999 17.6499L2.71451 12.0787C2.47772 11.9366 2.40093 11.6295 2.54301 11.3927C2.58523 11.3223 2.64413 11.2634 2.71451 11.2212L3.9166 10.4999L11.9999 15.3499L20.0833 10.4999ZM12.5144 1.30864L21.2854 6.5712C21.5221 6.71327 21.5989 7.0204 21.4569 7.25719C21.4146 7.32757 21.3557 7.38647 21.2854 7.42869L11.9999 12.9999L2.71451 7.42869C2.47772 7.28662 2.40093 6.97949 2.54301 6.7427C2.58523 6.67232 2.64413 6.61343 2.71451 6.5712L11.4854 1.30864C11.8021 1.11864 12.1977 1.11864 12.5144 1.30864ZM11.9999 3.33233L5.88723 6.99995L11.9999 10.6676L18.1126 6.99995L11.9999 3.33233Z" fill="currentColor"/>`, "stack": `<path d="M20.0833 15.1999L21.2854 15.9212C21.5221 16.0633 21.5989 16.3704 21.4569 16.6072C21.4146 16.6776 21.3557 16.7365 21.2854 16.7787L12.5144 22.0412C12.1977 22.2313 11.8021 22.2313 11.4854 22.0412L2.71451 16.7787C2.47772 16.6366 2.40093 16.3295 2.54301 16.0927C2.58523 16.0223 2.64413 15.9634 2.71451 15.9212L3.9166 15.1999L11.9999 20.0499L20.0833 15.1999ZM20.0833 10.4999L21.2854 11.2212C21.5221 11.3633 21.5989 11.6704 21.4569 11.9072C21.4146 11.9776 21.3557 12.0365 21.2854 12.0787L11.9999 17.6499L2.71451 12.0787C2.47772 11.9366 2.40093 11.6295 2.54301 11.3927C2.58523 11.3223 2.64413 11.2634 2.71451 11.2212L3.9166 10.4999L11.9999 15.3499L20.0833 10.4999ZM12.5144 1.30864L21.2854 6.5712C21.5221 6.71327 21.5989 7.0204 21.4569 7.25719C21.4146 7.32757 21.3557 7.38647 21.2854 7.42869L11.9999 12.9999L2.71451 7.42869C2.47772 7.28662 2.40093 6.97949 2.54301 6.7427C2.58523 6.67232 2.64413 6.61343 2.71451 6.5712L11.4854 1.30864C11.8021 1.11864 12.1977 1.11864 12.5144 1.30864ZM11.9999 3.33233L5.88723 6.99995L11.9999 10.6676L18.1126 6.99995L11.9999 3.33233Z" fill="currentColor"/>`,
"star-fill": `<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z" fill="currentColor"/>`,
"star": `<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26ZM12.0006 15.968L16.2473 18.3451L15.2988 13.5717L18.8719 10.2674L14.039 9.69434L12.0006 5.27502L9.96214 9.69434L5.12921 10.2674L8.70231 13.5717L7.75383 18.3451L12.0006 15.968Z" fill="currentColor"/>`, "star": `<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26ZM12.0006 15.968L16.2473 18.3451L15.2988 13.5717L18.8719 10.2674L14.039 9.69434L12.0006 5.27502L9.96214 9.69434L5.12921 10.2674L8.70231 13.5717L7.75383 18.3451L12.0006 15.968Z" fill="currentColor"/>`,
"star-fill": `<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z" fill="currentColor"/>`,
"sticky-note": `<path d="M21 15L15 20.996L4.00221 21C3.4487 21 3 20.5551 3 20.0066V3.9934C3 3.44476 3.44495 3 3.9934 3H20.0066C20.5552 3 21 3.45576 21 4.00247V15ZM19 5H5V19H13V14C13 13.4872 13.386 13.0645 13.8834 13.0067L14 13L19 12.999V5ZM18.171 14.999L15 15V18.169L18.171 14.999Z" fill="currentColor"/>`, "sticky-note": `<path d="M21 15L15 20.996L4.00221 21C3.4487 21 3 20.5551 3 20.0066V3.9934C3 3.44476 3.44495 3 3.9934 3H20.0066C20.5552 3 21 3.45576 21 4.00247V15ZM19 5H5V19H13V14C13 13.4872 13.386 13.0645 13.8834 13.0067L14 13L19 12.999V5ZM18.171 14.999L15 15V18.169L18.171 14.999Z" fill="currentColor"/>`,
"stop": `<path d="M7 7V17H17V7H7ZM6 5H18C18.5523 5 19 5.44772 19 6V18C19 18.5523 18.5523 19 18 19H6C5.44772 19 5 18.5523 5 18V6C5 5.44772 5.44772 5 6 5Z" fill="currentColor"/>`, "stop": `<path d="M7 7V17H17V7H7ZM6 5H18C18.5523 5 19 5.44772 19 6V18C19 18.5523 18.5523 19 18 19H6C5.44772 19 5 18.5523 5 18V6C5 5.44772 5.44772 5 6 5Z" fill="currentColor"/>`,
"subtract": `<path d="M5 11V13H19V11H5Z" fill="currentColor"/>`, "subtract": `<path d="M5 11V13H19V11H5Z" fill="currentColor"/>`,
"survey": `<path d="M17 2V4H20.0066C20.5552 4 21 4.44495 21 4.9934V21.0066C21 21.5552 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5551 3 21.0066V4.9934C3 4.44476 3.44495 4 3.9934 4H7V2H17ZM7 6H5V20H19V6H17V8H7V6ZM9 16V18H7V16H9ZM9 13V15H7V13H9ZM9 10V12H7V10H9ZM15 4H9V6H15V4Z" fill="currentColor"/>`, "survey": `<path d="M17 2V4H20.0066C20.5552 4 21 4.44495 21 4.9934V21.0066C21 21.5552 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5551 3 21.0066V4.9934C3 4.44476 3.44495 4 3.9934 4H7V2H17ZM7 6H5V20H19V6H17V8H7V6ZM9 16V18H7V16H9ZM9 13V15H7V13H9ZM9 10V12H7V10H9ZM15 4H9V6H15V4Z" fill="currentColor"/>`,
"target-fill": `<path d="M12 2C12.5523 2 13 2.44772 13 3C13 3.55228 12.5523 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 11.4477 20.4477 11 21 11C21.5523 11 22 11.4477 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 6C12.5523 6 13 6.44772 13 7C13 7.55228 12.5523 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 11.4477 16.4477 11 17 11C17.5523 11 18 11.4477 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6ZM18.5713 2.10059C18.8474 2.1006 19.0712 2.32449 19.0713 2.60059V4.42969C19.0716 4.70553 19.2954 4.92866 19.5713 4.92871H21.3994C21.6754 4.92871 21.8992 5.15275 21.8994 5.42871V6.34375L20.0107 8.23242C19.6358 8.60719 19.1268 8.81824 18.5967 8.81836H16.5967L12.707 12.707C12.3165 13.0974 11.6835 13.0975 11.293 12.707C10.9027 12.3165 10.9026 11.6834 11.293 11.293L15.1826 7.4043V5.4043C15.1826 4.87411 15.3928 4.36526 15.7676 3.99023L17.6572 2.10059H18.5713Z" fill="currentColor"/>`,
"target": `<path d="M12 1.99999C12.5523 1.99999 13 2.4477 13 2.99999C12.9999 3.55224 12.5522 3.99999 12 3.99999C7.58172 3.99999 4 7.58171 4 12C4.00004 16.4182 7.58174 20 12 20C16.4182 20 19.9999 16.4182 20 12C20 11.4477 20.4477 11 21 11C21.5523 11 22 11.4477 22 12C21.9999 17.5228 17.5228 22 12 22C6.47717 22 2.00004 17.5228 2 12C2 6.47714 6.47715 1.99999 12 1.99999ZM12 5.99999C12.5523 5.99999 13 6.4477 13 6.99999C12.9999 7.55224 12.5522 7.99999 12 7.99999C9.79085 7.99999 7.99999 9.79085 7.99999 12C8.00004 14.2091 9.79088 16 12 16C14.2091 16 15.9999 14.2091 16 12C16 11.4477 16.4477 11 17 11C17.5523 11 18 11.4477 18 12C17.9999 15.3137 15.3137 18 12 18C8.68631 18 6.00004 15.3137 6 12C6 8.68628 8.68629 5.99999 12 5.99999ZM17.6562 2.10057C18.0468 1.71005 18.6807 1.71005 19.0713 2.10057C19.4614 2.49105 19.4615 3.12419 19.0713 3.51463L18.3633 4.22069L18.3642 4.22167C17.9737 4.61219 17.9737 5.2452 18.3642 5.63573C18.7548 6.02612 19.3878 6.02621 19.7783 5.63573L20.4853 4.9287C20.8759 4.53839 21.5089 4.53826 21.8994 4.9287C22.2899 5.31915 22.2897 5.95222 21.8994 6.34276L19.7783 8.46483C19.5909 8.65223 19.3363 8.75671 19.0713 8.75682H16.6572L12.707 12.707C12.3165 13.0974 11.6834 13.0974 11.293 12.707C10.9025 12.3165 10.9026 11.6835 11.293 11.293L15.2422 7.34374V4.9287C15.2422 4.66356 15.3477 4.40916 15.5351 4.22167L17.6562 2.10057Z" fill="currentColor"/>`, "target": `<path d="M12 1.99999C12.5523 1.99999 13 2.4477 13 2.99999C12.9999 3.55224 12.5522 3.99999 12 3.99999C7.58172 3.99999 4 7.58171 4 12C4.00004 16.4182 7.58174 20 12 20C16.4182 20 19.9999 16.4182 20 12C20 11.4477 20.4477 11 21 11C21.5523 11 22 11.4477 22 12C21.9999 17.5228 17.5228 22 12 22C6.47717 22 2.00004 17.5228 2 12C2 6.47714 6.47715 1.99999 12 1.99999ZM12 5.99999C12.5523 5.99999 13 6.4477 13 6.99999C12.9999 7.55224 12.5522 7.99999 12 7.99999C9.79085 7.99999 7.99999 9.79085 7.99999 12C8.00004 14.2091 9.79088 16 12 16C14.2091 16 15.9999 14.2091 16 12C16 11.4477 16.4477 11 17 11C17.5523 11 18 11.4477 18 12C17.9999 15.3137 15.3137 18 12 18C8.68631 18 6.00004 15.3137 6 12C6 8.68628 8.68629 5.99999 12 5.99999ZM17.6562 2.10057C18.0468 1.71005 18.6807 1.71005 19.0713 2.10057C19.4614 2.49105 19.4615 3.12419 19.0713 3.51463L18.3633 4.22069L18.3642 4.22167C17.9737 4.61219 17.9737 5.2452 18.3642 5.63573C18.7548 6.02612 19.3878 6.02621 19.7783 5.63573L20.4853 4.9287C20.8759 4.53839 21.5089 4.53826 21.8994 4.9287C22.2899 5.31915 22.2897 5.95222 21.8994 6.34276L19.7783 8.46483C19.5909 8.65223 19.3363 8.75671 19.0713 8.75682H16.6572L12.707 12.707C12.3165 13.0974 11.6834 13.0974 11.293 12.707C10.9025 12.3165 10.9026 11.6835 11.293 11.293L15.2422 7.34374V4.9287C15.2422 4.66356 15.3477 4.40916 15.5351 4.22167L17.6562 2.10057Z" fill="currentColor"/>`,
"target-fill": `<path d="M12 2C12.5523 2 13 2.44772 13 3C13 3.55228 12.5523 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 11.4477 20.4477 11 21 11C21.5523 11 22 11.4477 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 6C12.5523 6 13 6.44772 13 7C13 7.55228 12.5523 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 11.4477 16.4477 11 17 11C17.5523 11 18 11.4477 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6ZM18.5713 2.10059C18.8474 2.1006 19.0712 2.32449 19.0713 2.60059V4.42969C19.0716 4.70553 19.2954 4.92866 19.5713 4.92871H21.3994C21.6754 4.92871 21.8992 5.15275 21.8994 5.42871V6.34375L20.0107 8.23242C19.6358 8.60719 19.1268 8.81824 18.5967 8.81836H16.5967L12.707 12.707C12.3165 13.0974 11.6835 13.0975 11.293 12.707C10.9027 12.3165 10.9026 11.6834 11.293 11.293L15.1826 7.4043V5.4043C15.1826 4.87411 15.3928 4.36526 15.7676 3.99023L17.6572 2.10059H18.5713Z" fill="currentColor"/>`,
"task": `<path d="M19 4H5V20H19V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H19.9997C20.5519 2 20.9996 2.44772 20.9997 3L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11.2929 13.1213L15.5355 8.87868L16.9497 10.2929L11.2929 15.9497L7.40381 12.0607L8.81802 10.6464L11.2929 13.1213Z" fill="currentColor"/>`, "task": `<path d="M19 4H5V20H19V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H19.9997C20.5519 2 20.9996 2.44772 20.9997 3L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11.2929 13.1213L15.5355 8.87868L16.9497 10.2929L11.2929 15.9497L7.40381 12.0607L8.81802 10.6464L11.2929 13.1213Z" fill="currentColor"/>`,
"terminal-box": `<path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM4 5V19H20V5H4ZM12 15H18V17H12V15ZM8.66685 12L5.83842 9.17157L7.25264 7.75736L11.4953 12L7.25264 16.2426L5.83842 14.8284L8.66685 12Z" fill="currentColor"/>`,
"terminal": `<path d="M10.9999 12L3.92886 19.0711L2.51465 17.6569L8.1715 12L2.51465 6.34317L3.92886 4.92896L10.9999 12ZM10.9999 19H20.9999V21H10.9999V19Z" fill="currentColor"/>`, "terminal": `<path d="M10.9999 12L3.92886 19.0711L2.51465 17.6569L8.1715 12L2.51465 6.34317L3.92886 4.92896L10.9999 12ZM10.9999 19H20.9999V21H10.9999V19Z" fill="currentColor"/>`,
"terminal-box": `<path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM4 5V19H20V5H4ZM12 15H18V17H12V15ZM8.66685 12L5.83842 9.17157L7.25264 7.75736L11.4953 12L7.25264 16.2426L5.83842 14.8284L8.66685 12Z" fill="currentColor"/>`,
"terminal-window": `<path d="M20 9V5H4V9H20ZM20 11H4V19H20V11ZM3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM5 12H8V17H5V12ZM5 6H7V8H5V6ZM9 6H11V8H9V6Z" fill="currentColor"/>`, "terminal-window": `<path d="M20 9V5H4V9H20ZM20 11H4V19H20V11ZM3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM5 12H8V17H5V12ZM5 6H7V8H5V6ZM9 6H11V8H9V6Z" fill="currentColor"/>`,
"text": `<path d="M13 6V21H11V6H5V4H19V6H13Z" fill="currentColor"/>`, "text": `<path d="M13 6V21H11V6H5V4H19V6H13Z" fill="currentColor"/>`,
"text-wrap": `<path d="M15 18H16.5C17.8807 18 19 16.8807 19 15.5C19 14.1193 17.8807 13 16.5 13H3V11H16.5C18.9853 11 21 13.0147 21 15.5C21 17.9853 18.9853 20 16.5 20H15V22L11 19L15 16V18ZM3 4H21V6H3V4ZM9 18V20H3V18H9Z" fill="currentColor"/>`, "text-wrap": `<path d="M15 18H16.5C17.8807 18 19 16.8807 19 15.5C19 14.1193 17.8807 13 16.5 13H3V11H16.5C18.9853 11 21 13.0147 21 15.5C21 17.9853 18.9853 20 16.5 20H15V22L11 19L15 16V18ZM3 4H21V6H3V4ZM9 18V20H3V18H9Z" fill="currentColor"/>`,
@@ -230,8 +231,8 @@ export const iconSpriteData = {
"tools": `<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z" fill="currentColor"/>`, "tools": `<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z" fill="currentColor"/>`,
"twitter-xfill": `<path d="M17.6874 3.0625L12.6907 8.77425L8.37045 3.0625H2.11328L9.58961 12.8387L2.50378 20.9375H5.53795L11.0068 14.6886L15.7863 20.9375H21.8885L14.095 10.6342L20.7198 3.0625H17.6874ZM16.6232 19.1225L5.65436 4.78217H7.45745L18.3034 19.1225H16.6232Z" fill="currentColor"/>`, "twitter-xfill": `<path d="M17.6874 3.0625L12.6907 8.77425L8.37045 3.0625H2.11328L9.58961 12.8387L2.50378 20.9375H5.53795L11.0068 14.6886L15.7863 20.9375H21.8885L14.095 10.6342L20.7198 3.0625H17.6874ZM16.6232 19.1225L5.65436 4.78217H7.45745L18.3034 19.1225H16.6232Z" fill="currentColor"/>`,
"unpin": `<path d="M20.9701 17.1716 19.5559 18.5858 16.0214 15.0513 15.9476 15.1251 15.2405 18.6606 13.8263 20.0748 9.58369 15.8322 4.63394 20.7819 3.21973 19.3677 8.16947 14.418 3.92683 10.1753 5.34105 8.7611 8.87658 8.05399 8.95029 7.98028 5.41373 4.44371 6.82794 3.0295 20.9701 17.1716ZM10.3645 9.39449 9.86261 9.8964 7.04072 10.4608 13.5409 16.9609 14.1052 14.139 14.6071 13.6371 10.3645 9.39449ZM18.7761 9.46821 17.4356 10.8087 18.8498 12.2229 20.1903 10.8824 20.8974 11.5895 22.3116 10.1753 13.8263 1.69003 12.4121 3.10425 13.1192 3.81135 11.7787 5.15185 13.1929 6.56607 14.5334 5.22557 18.7761 9.46821Z" fill="currentColor"/>`, "unpin": `<path d="M20.9701 17.1716 19.5559 18.5858 16.0214 15.0513 15.9476 15.1251 15.2405 18.6606 13.8263 20.0748 9.58369 15.8322 4.63394 20.7819 3.21973 19.3677 8.16947 14.418 3.92683 10.1753 5.34105 8.7611 8.87658 8.05399 8.95029 7.98028 5.41373 4.44371 6.82794 3.0295 20.9701 17.1716ZM10.3645 9.39449 9.86261 9.8964 7.04072 10.4608 13.5409 16.9609 14.1052 14.139 14.6071 13.6371 10.3645 9.39449ZM18.7761 9.46821 17.4356 10.8087 18.8498 12.2229 20.1903 10.8824 20.8974 11.5895 22.3116 10.1753 13.8263 1.69003 12.4121 3.10425 13.1192 3.81135 11.7787 5.15185 13.1929 6.56607 14.5334 5.22557 18.7761 9.46821Z" fill="currentColor"/>`,
"user-3": `<path d="M20 22H18V20C18 18.3431 16.6569 17 15 17H9C7.34315 17 6 18.3431 6 20V22H4V20C4 17.2386 6.23858 15 9 15H15C17.7614 15 20 17.2386 20 20V22ZM12 13C8.68629 13 6 10.3137 6 7C6 3.68629 8.68629 1 12 1C15.3137 1 18 3.68629 18 7C18 10.3137 15.3137 13 12 13ZM12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" fill="currentColor"/>`,
"user": `<path d="M4 22C4 17.5817 7.58172 14 12 14C16.4183 14 20 17.5817 20 22H18C18 18.6863 15.3137 16 12 16C8.68629 16 6 18.6863 6 22H4ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13ZM12 11C14.21 11 16 9.21 16 7C16 4.79 14.21 3 12 3C9.79 3 8 4.79 8 7C8 9.21 9.79 11 12 11Z" fill="currentColor"/>`, "user": `<path d="M4 22C4 17.5817 7.58172 14 12 14C16.4183 14 20 17.5817 20 22H18C18 18.6863 15.3137 16 12 16C8.68629 16 6 18.6863 6 22H4ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13ZM12 11C14.21 11 16 9.21 16 7C16 4.79 14.21 3 12 3C9.79 3 8 4.79 8 7C8 9.21 9.79 11 12 11Z" fill="currentColor"/>`,
"user-3": `<path d="M20 22H18V20C18 18.3431 16.6569 17 15 17H9C7.34315 17 6 18.3431 6 20V22H4V20C4 17.2386 6.23858 15 9 15H15C17.7614 15 20 17.2386 20 20V22ZM12 13C8.68629 13 6 10.3137 6 7C6 3.68629 8.68629 1 12 1C15.3137 1 18 3.68629 18 7C18 10.3137 15.3137 13 12 13ZM12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" fill="currentColor"/>`,
"volume-up": `<path d="M6.60282 10.0001L10 7.22056V16.7796L6.60282 14.0001H3V10.0001H6.60282ZM2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z" fill="currentColor"/>`, "volume-up": `<path d="M6.60282 10.0001L10 7.22056V16.7796L6.60282 14.0001H3V10.0001H6.60282ZM2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z" fill="currentColor"/>`,
"window": `<path d="M21 3C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM20 11H4V19H20V11ZM20 5H4V9H20V5ZM11 6V8H9V6H11ZM7 6V8H5V6H7Z" fill="currentColor"/>`, "window": `<path d="M21 3C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM20 11H4V19H20V11ZM20 5H4V9H20V5ZM11 6V8H9V6H11ZM7 6V8H5V6H7Z" fill="currentColor"/>`,
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
@@ -24,6 +24,8 @@ export const OpenCodeCliSettings: React.FC = () => {
const [isSaving, setIsSaving] = React.useState(false); const [isSaving, setIsSaving] = React.useState(false);
const showOpenCodeUpdateNotifications = useUIStore((state) => state.showOpenCodeUpdateNotifications); const showOpenCodeUpdateNotifications = useUIStore((state) => state.showOpenCodeUpdateNotifications);
const setShowOpenCodeUpdateNotifications = useUIStore((state) => state.setShowOpenCodeUpdateNotifications); const setShowOpenCodeUpdateNotifications = useUIStore((state) => state.setShowOpenCodeUpdateNotifications);
const agentControlToolEnabled = useUIStore((state) => state.agentControlToolEnabled);
const setAgentControlToolEnabled = useUIStore((state) => state.setAgentControlToolEnabled);
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -101,6 +103,11 @@ export const OpenCodeCliSettings: React.FC = () => {
void updateDesktopSettings({ showOpenCodeUpdateNotifications: enabled }); void updateDesktopSettings({ showOpenCodeUpdateNotifications: enabled });
}, [setShowOpenCodeUpdateNotifications]); }, [setShowOpenCodeUpdateNotifications]);
const handleAgentControlToolChange = React.useCallback((enabled: boolean) => {
setAgentControlToolEnabled(enabled);
void updateDesktopSettings({ agentControlToolEnabled: enabled });
}, [setAgentControlToolEnabled]);
return ( return (
<SettingsSection title={t('settings.openchamber.opencodeCli.title')}> <SettingsSection title={t('settings.openchamber.opencodeCli.title')}>
<div className="space-y-0.5"> <div className="space-y-0.5">
@@ -152,6 +159,15 @@ export const OpenCodeCliSettings: React.FC = () => {
ariaLabel={t('settings.openchamber.opencodeCli.field.showUpdateNotificationsAria')} ariaLabel={t('settings.openchamber.opencodeCli.field.showUpdateNotificationsAria')}
/> />
<SettingsCheckboxRow
settingsItem="sessions.agent-control-tool"
checked={agentControlToolEnabled}
onChange={handleAgentControlToolChange}
label={t('settings.openchamber.opencodeCli.field.agentControlTool')}
ariaLabel={t('settings.openchamber.opencodeCli.field.agentControlToolAria')}
info={t('settings.openchamber.opencodeCli.field.agentControlToolInfo')}
/>
<div className="flex justify-start py-1.5"> <div className="flex justify-start py-1.5">
<Button <Button
type="button" type="button"
@@ -494,6 +494,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const [resolvedWorktreeTopologyKey, setResolvedWorktreeTopologyKey] = React.useState<string | null>( const [resolvedWorktreeTopologyKey, setResolvedWorktreeTopologyKey] = React.useState<string | null>(
isVSCode ? projectWorktreeDiscoveryKey : null, isVSCode ? projectWorktreeDiscoveryKey : null,
); );
const [worktreeDiscoveryRevision, requestWorktreeDiscovery] = React.useReducer((revision) => revision + 1, 0);
const isWorktreeTopologyLoading = !isVSCode && resolvedWorktreeTopologyKey !== projectWorktreeDiscoveryKey; const isWorktreeTopologyLoading = !isVSCode && resolvedWorktreeTopologyKey !== projectWorktreeDiscoveryKey;
const [unresolvedWorktreeProjectPaths, setUnresolvedWorktreeProjectPaths] = React.useState<ReadonlySet<string>>(new Set()); const [unresolvedWorktreeProjectPaths, setUnresolvedWorktreeProjectPaths] = React.useState<ReadonlySet<string>>(new Set());
@@ -591,19 +592,35 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [isVSCode, projectWorktreeDiscoveryKey, runtimeKey]); }, [isVSCode, projectWorktreeDiscoveryKey, runtimeKey, worktreeDiscoveryRevision]);
React.useEffect(() => { React.useEffect(() => {
let refreshTimeout: ReturnType<typeof setTimeout> | null = null; let refreshTimeout: ReturnType<typeof setTimeout> | null = null;
let needsGlobalRefresh = false;
const sessionDirectories = new Set<string>();
const unsubscribe = subscribeOpenchamberEvents((event) => { const unsubscribe = subscribeOpenchamberEvents((event) => {
if (event.type !== 'scheduled-task-ran') { if (event.type === 'scheduled-task-ran') {
return; needsGlobalRefresh = true;
} else {
sessionDirectories.add(event.directory);
requestWorktreeDiscovery();
} }
if (refreshTimeout) { if (refreshTimeout) {
clearTimeout(refreshTimeout); clearTimeout(refreshTimeout);
} }
refreshTimeout = setTimeout(() => { refreshTimeout = setTimeout(() => {
void refreshGlobalSessions(syncSessionsSnapshotRef.current); refreshTimeout = null;
if (needsGlobalRefresh) {
needsGlobalRefresh = false;
sessionDirectories.clear();
void refreshGlobalSessions(syncSessionsSnapshotRef.current);
return;
}
const directories = [...sessionDirectories];
sessionDirectories.clear();
if (directories.length > 0) {
void refreshGlobalSessionsForDirectories(directories, syncSessionsSnapshotRef.current);
}
}, 500); }, 500);
}); });
return () => { return () => {
@@ -73,6 +73,7 @@
- Folder membership may contain both a parent session and its descendants. Rendering treats only the highest assigned ancestors as folder roots because their normal session trees already include assigned descendants; persisted membership remains unchanged for cleanup and move semantics. - Folder membership may contain both a parent session and its descendants. Rendering treats only the highest assigned ancestors as folder roots because their normal session trees already include assigned descendants; persisted membership remains unchanged for cleanup and move semantics.
- Sidebar selection holds the clicked row's viewport position across navigation-driven sidebar updates. Wheel or touch input cancels the hold immediately, so programmatic compensation never fights intentional scrolling. - Sidebar selection holds the clicked row's viewport position across navigation-driven sidebar updates. Wheel or touch input cancels the hold immediately, so programmatic compensation never fights intentional scrolling.
- Global session subscriptions are structural: create/delete, title, share, archive, directory, parent, and slug changes invalidate the tree. Recency-only `time.updated` changes do not trigger a rebuild. The separate lifecycle rank invalidates ordering only on `settled ↔ active` transitions, with root sessions ranked among roots and child sessions only among siblings of the same parent. - Global session subscriptions are structural: create/delete, title, share, archive, directory, parent, and slug changes invalidate the tree. Recency-only `time.updated` changes do not trigger a rebuild. The separate lifecycle rank invalidates ordering only on `settled ↔ active` transitions, with root sessions ranked among roots and child sessions only among siblings of the same parent.
- CLI/server-created sessions use the low-frequency OpenChamber control event stream to refresh only the created session directory. The same event retriggers bounded worktree discovery so a newly created external worktree gains ownership without a view reload; it does not re-enable broad session or streaming subscriptions.
- Recent membership includes active root sessions immediately even when their last committed `time.updated` falls outside the 48-hour window. Children and archived sessions remain excluded, and inactive roots remain timestamp-based. The active-ID subscription is disabled while the sidebar is hidden and ignores retry/status detail changes, avoiding streaming-frequency rerenders. - Recent membership includes active root sessions immediately even when their last committed `time.updated` falls outside the 48-hour window. Children and archived sessions remain excluded, and inactive roots remain timestamp-based. The active-ID subscription is disabled while the sidebar is hidden and ignores retry/status detail changes, avoiding streaming-frequency rerenders.
- Structural updates rebuild grouped nodes only for projects whose local sessions, worktrees, repository state, or branch changed; unchanged project sections preserve references so memoized group/session descendants skip the update wave. - Structural updates rebuild grouped nodes only for projects whose local sessions, worktrees, repository state, or branch changed; unchanged project sections preserve references so memoized group/session descendants skip the update wave.
- Empty successful lists, unresolved loads, and failed loads are separate UI states. Failed groups expose Retry and retain prior data. - Empty successful lists, unresolved loads, and failed loads are separate UI states. Failed groups expose Retry and retain prior data.
+2
View File
@@ -143,6 +143,7 @@ export type DesktopSettings = {
desktopWindowControlsPosition?: DesktopWindowControlsPosition; desktopWindowControlsPosition?: DesktopWindowControlsPosition;
inputSpellcheckEnabled?: boolean; inputSpellcheckEnabled?: boolean;
showOpenCodeUpdateNotifications?: boolean; showOpenCodeUpdateNotifications?: boolean;
agentControlToolEnabled?: boolean;
openCodeUpdateToastDismissedVersion?: string; openCodeUpdateToastDismissedVersion?: string;
showToolFileIcons?: boolean; showToolFileIcons?: boolean;
codeBlockLineWrap?: boolean; codeBlockLineWrap?: boolean;
@@ -208,6 +209,7 @@ export type DesktopSettings = {
draftStarters?: DraftStarterRef[]; draftStarters?: DraftStarterRef[];
// One-time migration marker: Craft a Goal was offered in the starter row. // One-time migration marker: Craft a Goal was offered in the starter row.
draftStartersCraftGoalAdded?: boolean; draftStartersCraftGoalAdded?: boolean;
draftStartersScheduleTaskAdded?: boolean;
}; };
type DesktopBridgeGlobal = { type DesktopBridgeGlobal = {
+1
View File
@@ -28,6 +28,7 @@ export const BUILTIN_STARTERS: readonly BuiltInStarter[] = [
{ name: 'weigh', icon: 'scales-3', labelKey: 'chat.draftPresets.weigh.label', command: '/weigh' }, { name: 'weigh', icon: 'scales-3', labelKey: 'chat.draftPresets.weigh.label', command: '/weigh' },
{ name: 'plan-feature', icon: 'survey', labelKey: 'chat.draftPresets.plan.label', command: '/plan-feature' }, { name: 'plan-feature', icon: 'survey', labelKey: 'chat.draftPresets.plan.label', command: '/plan-feature' },
{ name: 'craft-goal', icon: 'target', labelKey: 'chat.draftPresets.craftGoal.label', command: '/craft-goal' }, { name: 'craft-goal', icon: 'target', labelKey: 'chat.draftPresets.craftGoal.label', command: '/craft-goal' },
{ name: 'schedule-task', icon: 'calendar-schedule', labelKey: 'chat.draftPresets.scheduleTask.label', command: '/schedule-task' },
{ name: 'debug', icon: 'bug', labelKey: 'chat.draftPresets.debug.label', command: '/debug' }, { name: 'debug', icon: 'bug', labelKey: 'chat.draftPresets.debug.label', command: '/debug' },
{ name: 'review', icon: 'search-eye', labelKey: 'chat.draftPresets.review.label', command: '/workspace-review' }, { name: 'review', icon: 'search-eye', labelKey: 'chat.draftPresets.review.label', command: '/workspace-review' },
]; ];
@@ -987,6 +987,9 @@ export const settingsDict = {
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'Show OpenCode update notifications', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'Show OpenCode update notifications',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'Show OpenCode update notifications', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'Show OpenCode update notifications',
'settings.openchamber.opencodeCli.field.agentControlTool': 'Agent control tool',
'settings.openchamber.opencodeCli.field.agentControlToolAria': 'Enable the agent control tool',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': 'Let agents orchestrate your work from chat: spin up sessions and worktrees, delegate prompts to other agents, and manage scheduled tasks. Adds a small tool description to each session. Applies after Save + Reload.',
'settings.openchamber.opencodeCli.actions.browseAria': 'Browse for OpenCode binary path', 'settings.openchamber.opencodeCli.actions.browseAria': 'Browse for OpenCode binary path',
'settings.openchamber.opencodeCli.actions.browse': 'Browse', 'settings.openchamber.opencodeCli.actions.browse': 'Browse',
'settings.openchamber.opencodeCli.actions.saveAndReload': 'Save + Reload', 'settings.openchamber.opencodeCli.actions.saveAndReload': 'Save + Reload',
+3
View File
@@ -1759,6 +1759,7 @@ export const dict = {
'chat.draftPresets.weigh.label': 'Weigh my options', 'chat.draftPresets.weigh.label': 'Weigh my options',
'chat.draftPresets.plan.label': 'Start feature planning', 'chat.draftPresets.plan.label': 'Start feature planning',
'chat.draftPresets.craftGoal.label': 'Craft a Goal', 'chat.draftPresets.craftGoal.label': 'Craft a Goal',
'chat.draftPresets.scheduleTask.label': 'Schedule a Task',
'chat.draftPresets.debug.label': 'Debug an issue', 'chat.draftPresets.debug.label': 'Debug an issue',
'chat.draftPresets.review.label': 'Review my changes', 'chat.draftPresets.review.label': 'Review my changes',
'chat.draftStarters.add': 'Add a starter', 'chat.draftStarters.add': 'Add a starter',
@@ -1835,7 +1836,9 @@ export const dict = {
'chat.commandAutocomplete.command.handoffReviewDescription': 'Create or reuse a separate review session from a generated handoff.', 'chat.commandAutocomplete.command.handoffReviewDescription': 'Create or reuse a separate review session from a generated handoff.',
'chat.commandAutocomplete.command.featurePlanDescription': 'Start a guided, back-and-forth planning session for a new feature.', 'chat.commandAutocomplete.command.featurePlanDescription': 'Start a guided, back-and-forth planning session for a new feature.',
'chat.commandAutocomplete.command.craftGoalDescription': 'Turn an idea or task into a clear, verifiable Goal.', 'chat.commandAutocomplete.command.craftGoalDescription': 'Turn an idea or task into a clear, verifiable Goal.',
'chat.commandAutocomplete.command.scheduleTaskDescription': 'Define a scheduled task through a guided dialogue.',
'chat.chatInput.toast.craftGoalFailed': 'Failed to start Goal crafting', 'chat.chatInput.toast.craftGoalFailed': 'Failed to start Goal crafting',
'chat.chatInput.toast.scheduleTaskFailed': 'Failed to start scheduled task setup',
'chat.commandAutocomplete.command.catchUpDescription': 'Re-establish context: what you were doing and where to pick up.', 'chat.commandAutocomplete.command.catchUpDescription': 'Re-establish context: what you were doing and where to pick up.',
'chat.commandAutocomplete.command.debugDescription': 'Guided root-cause investigation for a bug before proposing a fix.', 'chat.commandAutocomplete.command.debugDescription': 'Guided root-cause investigation for a bug before proposing a fix.',
'chat.commandAutocomplete.command.weighDescription': 'Weigh 2-3 approaches with trade-offs and a recommendation before you commit.', 'chat.commandAutocomplete.command.weighDescription': 'Weigh 2-3 approaches with trade-offs and a recommendation before you commit.',
@@ -954,6 +954,9 @@ export const settingsDict = {
"settings.openchamber.opencodeCli.field.binaryPathPlaceholder": "/Users/you/.bun/bin/opencode", "settings.openchamber.opencodeCli.field.binaryPathPlaceholder": "/Users/you/.bun/bin/opencode",
"settings.openchamber.opencodeCli.field.showUpdateNotifications": "Mostrar notificaciones de actualización de OpenCode", "settings.openchamber.opencodeCli.field.showUpdateNotifications": "Mostrar notificaciones de actualización de OpenCode",
"settings.openchamber.opencodeCli.field.showUpdateNotificationsAria": "Mostrar notificaciones de actualización de OpenCode", "settings.openchamber.opencodeCli.field.showUpdateNotificationsAria": "Mostrar notificaciones de actualización de OpenCode",
"settings.openchamber.opencodeCli.field.agentControlTool": "Herramienta de control para agentes",
"settings.openchamber.opencodeCli.field.agentControlToolAria": "Activar la herramienta de control para agentes",
"settings.openchamber.opencodeCli.field.agentControlToolInfo": "Permite que los agentes orquesten tu trabajo desde el chat: crear sesiones y worktrees, delegar prompts a otros agentes y gestionar tareas programadas. Añade una pequeña descripción de herramienta a cada sesión. Se aplica tras Save + Reload.",
"settings.openchamber.opencodeCli.actions.browseAria": "Buscar ruta del ejecutable de OpenCode", "settings.openchamber.opencodeCli.actions.browseAria": "Buscar ruta del ejecutable de OpenCode",
"settings.openchamber.opencodeCli.actions.browse": "Buscar", "settings.openchamber.opencodeCli.actions.browse": "Buscar",
"settings.openchamber.opencodeCli.actions.saveAndReload": "Guardar + Recargar", "settings.openchamber.opencodeCli.actions.saveAndReload": "Guardar + Recargar",
+3
View File
@@ -1737,6 +1737,7 @@ export const dict: Record<I18nKey, string> = {
"chat.draftPresets.weigh.label": "Weigh my options", "chat.draftPresets.weigh.label": "Weigh my options",
"chat.draftPresets.plan.label": "Start feature planning", "chat.draftPresets.plan.label": "Start feature planning",
"chat.draftPresets.craftGoal.label": "Crear un Goal", "chat.draftPresets.craftGoal.label": "Crear un Goal",
"chat.draftPresets.scheduleTask.label": "Programar una tarea",
"chat.draftPresets.debug.label": "Debug an issue", "chat.draftPresets.debug.label": "Debug an issue",
"chat.draftPresets.review.label": "Review my changes", "chat.draftPresets.review.label": "Review my changes",
"chat.draftStarters.add": "Add a starter", "chat.draftStarters.add": "Add a starter",
@@ -1813,7 +1814,9 @@ export const dict: Record<I18nKey, string> = {
"chat.commandAutocomplete.command.handoffReviewDescription": "Crea o reutiliza una sesión de revisión separada a partir de un handoff generado.", "chat.commandAutocomplete.command.handoffReviewDescription": "Crea o reutiliza una sesión de revisión separada a partir de un handoff generado.",
"chat.commandAutocomplete.command.featurePlanDescription": "Inicia una sesión de planificación guiada e interactiva para una nueva función.", "chat.commandAutocomplete.command.featurePlanDescription": "Inicia una sesión de planificación guiada e interactiva para una nueva función.",
"chat.commandAutocomplete.command.craftGoalDescription": "Convierte una idea o tarea en un Goal claro y verificable.", "chat.commandAutocomplete.command.craftGoalDescription": "Convierte una idea o tarea en un Goal claro y verificable.",
"chat.commandAutocomplete.command.scheduleTaskDescription": "Define una tarea programada mediante un diálogo guiado.",
"chat.chatInput.toast.craftGoalFailed": "No se pudo iniciar la creación del Goal", "chat.chatInput.toast.craftGoalFailed": "No se pudo iniciar la creación del Goal",
"chat.chatInput.toast.scheduleTaskFailed": "No se pudo iniciar la configuración de la tarea programada",
"chat.commandAutocomplete.command.catchUpDescription": "Recupera el contexto: qué estabas haciendo y por dónde continuar.", "chat.commandAutocomplete.command.catchUpDescription": "Recupera el contexto: qué estabas haciendo y por dónde continuar.",
"chat.commandAutocomplete.command.debugDescription": "Investigación guiada de la causa raíz de un error antes de proponer una solución.", "chat.commandAutocomplete.command.debugDescription": "Investigación guiada de la causa raíz de un error antes de proponer una solución.",
"chat.commandAutocomplete.command.weighDescription": "Compara 2-3 enfoques con sus ventajas y desventajas y una recomendación antes de decidir.", "chat.commandAutocomplete.command.weighDescription": "Compara 2-3 enfoques con sus ventajas y desventajas y una recomendación antes de decidir.",
@@ -875,6 +875,9 @@ export const settingsDict = {
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'Afficher les notifications de mise à jour de OpenCode', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'Afficher les notifications de mise à jour de OpenCode',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'Afficher les notifications de mise à jour de OpenCode', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'Afficher les notifications de mise à jour de OpenCode',
'settings.openchamber.opencodeCli.field.agentControlTool': 'Outil de contrôle pour les agents',
'settings.openchamber.opencodeCli.field.agentControlToolAria': 'Activer l’outil de contrôle pour les agents',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': 'Laissez les agents orchestrer votre travail depuis le chat : créer des sessions et des worktrees, déléguer des prompts à d’autres agents et gérer les tâches planifiées. Ajoute une courte description d’outil à chaque session. Appliqué après Save + Reload.',
'settings.openchamber.opencodeCli.actions.browseAria': 'Rechercher le chemin binaire OpenCode', 'settings.openchamber.opencodeCli.actions.browseAria': 'Rechercher le chemin binaire OpenCode',
'settings.openchamber.opencodeCli.actions.browse': 'Parcourir', 'settings.openchamber.opencodeCli.actions.browse': 'Parcourir',
'settings.openchamber.opencodeCli.actions.saveAndReload': 'Enregistrer + Recharger', 'settings.openchamber.opencodeCli.actions.saveAndReload': 'Enregistrer + Recharger',
+3
View File
@@ -2750,6 +2750,7 @@ export const dict = {
'chat.draftPresets.weigh.label': 'Comparer mes options', 'chat.draftPresets.weigh.label': 'Comparer mes options',
'chat.draftPresets.plan.label': 'Lancer la planification de fonctionnalité', 'chat.draftPresets.plan.label': 'Lancer la planification de fonctionnalité',
'chat.draftPresets.craftGoal.label': 'Créer un Goal', 'chat.draftPresets.craftGoal.label': 'Créer un Goal',
'chat.draftPresets.scheduleTask.label': 'Planifier une tâche',
'chat.draftPresets.debug.label': 'Déboguer un problème', 'chat.draftPresets.debug.label': 'Déboguer un problème',
'chat.draftPresets.review.label': 'Revoir mes modifications', 'chat.draftPresets.review.label': 'Revoir mes modifications',
'chat.draftStarters.add': 'Ajouter un démarrage', 'chat.draftStarters.add': 'Ajouter un démarrage',
@@ -2762,7 +2763,9 @@ export const dict = {
'chat.commandAutocomplete.command.handoffReviewDescription': 'Créer ou réutiliser une session de revue séparée à partir d’un handoff généré.', 'chat.commandAutocomplete.command.handoffReviewDescription': 'Créer ou réutiliser une session de revue séparée à partir d’un handoff généré.',
'chat.commandAutocomplete.command.featurePlanDescription': 'Lancer une session guidée et interactive de planification pour une nouvelle fonctionnalité.', 'chat.commandAutocomplete.command.featurePlanDescription': 'Lancer une session guidée et interactive de planification pour une nouvelle fonctionnalité.',
'chat.commandAutocomplete.command.craftGoalDescription': 'Transformer une idée ou une tâche en Goal clair et vérifiable.', 'chat.commandAutocomplete.command.craftGoalDescription': 'Transformer une idée ou une tâche en Goal clair et vérifiable.',
'chat.commandAutocomplete.command.scheduleTaskDescription': 'Définir une tâche planifiée via un dialogue guidé.',
'chat.chatInput.toast.craftGoalFailed': 'Impossible de démarrer la création du Goal', 'chat.chatInput.toast.craftGoalFailed': 'Impossible de démarrer la création du Goal',
'chat.chatInput.toast.scheduleTaskFailed': 'Impossible de démarrer la configuration de la tâche planifiée',
'chat.commandAutocomplete.command.catchUpDescription': 'Rétablir le contexte : ce que vous faisiez et où reprendre.', 'chat.commandAutocomplete.command.catchUpDescription': 'Rétablir le contexte : ce que vous faisiez et où reprendre.',
'chat.commandAutocomplete.command.debugDescription': 'Investigation guidée de la cause racine d’un bug avant de proposer une correction.', 'chat.commandAutocomplete.command.debugDescription': 'Investigation guidée de la cause racine d’un bug avant de proposer une correction.',
'chat.commandAutocomplete.command.weighDescription': 'Comparer 2 à 3 approches avec compromis et recommandation avant de vous engager.', 'chat.commandAutocomplete.command.weighDescription': 'Comparer 2 à 3 approches avec compromis et recommandation avant de vous engager.',
@@ -987,6 +987,9 @@ export const settingsDict = {
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'OpenCode のアップデート通知を表示', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'OpenCode のアップデート通知を表示',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'OpenCode のアップデート通知を表示', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'OpenCode のアップデート通知を表示',
'settings.openchamber.opencodeCli.field.agentControlTool': 'エージェント制御ツール',
'settings.openchamber.opencodeCli.field.agentControlToolAria': 'エージェント制御ツールを有効にする',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': 'エージェントがチャットから作業をオーケストレーションできるようにします。セッションや worktree の作成、他のエージェントへのプロンプト委任、スケジュールタスクの管理が可能です。各セッションに小さなツール説明が追加されます。Save + Reload 後に適用されます。',
'settings.openchamber.opencodeCli.actions.browseAria': 'OpenCode バイナリパスを参照', 'settings.openchamber.opencodeCli.actions.browseAria': 'OpenCode バイナリパスを参照',
'settings.openchamber.opencodeCli.actions.browse': '参照', 'settings.openchamber.opencodeCli.actions.browse': '参照',
'settings.openchamber.opencodeCli.actions.saveAndReload': '保存して再読み込み', 'settings.openchamber.opencodeCli.actions.saveAndReload': '保存して再読み込み',
+3
View File
@@ -1755,6 +1755,7 @@ export const dict: Record<I18nKey, string> = {
'chat.draftPresets.weigh.label': '選択肢を比較検討', 'chat.draftPresets.weigh.label': '選択肢を比較検討',
'chat.draftPresets.plan.label': '機能の計画を開始', 'chat.draftPresets.plan.label': '機能の計画を開始',
'chat.draftPresets.craftGoal.label': 'Goal を作成', 'chat.draftPresets.craftGoal.label': 'Goal を作成',
'chat.draftPresets.scheduleTask.label': 'タスクをスケジュール',
'chat.draftPresets.debug.label': '問題をデバッグ', 'chat.draftPresets.debug.label': '問題をデバッグ',
'chat.draftPresets.review.label': '変更をレビュー', 'chat.draftPresets.review.label': '変更をレビュー',
'chat.draftStarters.add': 'スターターを追加', 'chat.draftStarters.add': 'スターターを追加',
@@ -1831,7 +1832,9 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.handoffReviewDescription': '生成されたハンドオフから別のレビューセッションを作成または再利用。', 'chat.commandAutocomplete.command.handoffReviewDescription': '生成されたハンドオフから別のレビューセッションを作成または再利用。',
'chat.commandAutocomplete.command.featurePlanDescription': '新機能のガイド付き対話型計画セッションを開始。', 'chat.commandAutocomplete.command.featurePlanDescription': '新機能のガイド付き対話型計画セッションを開始。',
'chat.commandAutocomplete.command.craftGoalDescription': 'アイデアやタスクを明確で検証可能な Goal に変換します。', 'chat.commandAutocomplete.command.craftGoalDescription': 'アイデアやタスクを明確で検証可能な Goal に変換します。',
'chat.commandAutocomplete.command.scheduleTaskDescription': '対話形式でスケジュールタスクを定義します。',
'chat.chatInput.toast.craftGoalFailed': 'Goal の作成を開始できませんでした', 'chat.chatInput.toast.craftGoalFailed': 'Goal の作成を開始できませんでした',
'chat.chatInput.toast.scheduleTaskFailed': 'スケジュールタスクの設定を開始できませんでした',
'chat.commandAutocomplete.command.catchUpDescription': 'コンテキストを再確立: 何をしていたか、どこから再開するか。', 'chat.commandAutocomplete.command.catchUpDescription': 'コンテキストを再確立: 何をしていたか、どこから再開するか。',
'chat.commandAutocomplete.command.debugDescription': '修正を提案する前に、バグのガイド付き根本原因調査。', 'chat.commandAutocomplete.command.debugDescription': '修正を提案する前に、バグのガイド付き根本原因調査。',
'chat.commandAutocomplete.command.weighDescription': 'トレードオフと推奨事項を含む2~3のアプローチを比較検討してからコミット。', 'chat.commandAutocomplete.command.weighDescription': 'トレードオフと推奨事項を含む2~3のアプローチを比較検討してからコミット。',
@@ -954,6 +954,9 @@ export const settingsDict = {
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'OpenCode 업데이트 알림 표시', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'OpenCode 업데이트 알림 표시',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'OpenCode 업데이트 알림 표시', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'OpenCode 업데이트 알림 표시',
'settings.openchamber.opencodeCli.field.agentControlTool': '에이전트 제어 도구',
'settings.openchamber.opencodeCli.field.agentControlToolAria': '에이전트 제어 도구 활성화',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': '에이전트가 채팅에서 작업을 오케스트레이션할 수 있습니다. 세션과 worktree 생성, 다른 에이전트에게 프롬프트 위임, 예약 작업 관리가 가능합니다. 각 세션에 작은 도구 설명이 추가됩니다. Save + Reload 후 적용됩니다.',
'settings.openchamber.opencodeCli.actions.browseAria': 'OpenCode binary 경로 찾아보기', 'settings.openchamber.opencodeCli.actions.browseAria': 'OpenCode binary 경로 찾아보기',
'settings.openchamber.opencodeCli.actions.browse': '찾아보기', 'settings.openchamber.opencodeCli.actions.browse': '찾아보기',
'settings.openchamber.opencodeCli.actions.saveAndReload': '저장 + 다시 로드', 'settings.openchamber.opencodeCli.actions.saveAndReload': '저장 + 다시 로드',
+3
View File
@@ -1761,6 +1761,7 @@ export const dict: Record<I18nKey, string> = {
'chat.draftPresets.weigh.label': 'Weigh my options', 'chat.draftPresets.weigh.label': 'Weigh my options',
'chat.draftPresets.plan.label': 'Start feature planning', 'chat.draftPresets.plan.label': 'Start feature planning',
'chat.draftPresets.craftGoal.label': 'Goal 만들기', 'chat.draftPresets.craftGoal.label': 'Goal 만들기',
'chat.draftPresets.scheduleTask.label': '작업 예약하기',
'chat.draftPresets.debug.label': 'Debug an issue', 'chat.draftPresets.debug.label': 'Debug an issue',
'chat.draftPresets.review.label': 'Review my changes', 'chat.draftPresets.review.label': 'Review my changes',
'chat.draftStarters.add': 'Add a starter', 'chat.draftStarters.add': 'Add a starter',
@@ -1837,7 +1838,9 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.handoffReviewDescription': '생성된 인수인계로 별도의 리뷰 세션을 만들거나 재사용합니다.', 'chat.commandAutocomplete.command.handoffReviewDescription': '생성된 인수인계로 별도의 리뷰 세션을 만들거나 재사용합니다.',
'chat.commandAutocomplete.command.featurePlanDescription': '새 기능을 위한 대화형 가이드 계획 세션을 시작합니다.', 'chat.commandAutocomplete.command.featurePlanDescription': '새 기능을 위한 대화형 가이드 계획 세션을 시작합니다.',
'chat.commandAutocomplete.command.craftGoalDescription': '아이디어나 작업을 명확하고 검증 가능한 Goal로 만듭니다.', 'chat.commandAutocomplete.command.craftGoalDescription': '아이디어나 작업을 명확하고 검증 가능한 Goal로 만듭니다.',
'chat.commandAutocomplete.command.scheduleTaskDescription': '안내형 대화를 통해 예약 작업을 정의합니다.',
'chat.chatInput.toast.craftGoalFailed': 'Goal 만들기를 시작하지 못했습니다', 'chat.chatInput.toast.craftGoalFailed': 'Goal 만들기를 시작하지 못했습니다',
'chat.chatInput.toast.scheduleTaskFailed': '예약 작업 설정을 시작하지 못했습니다',
'chat.commandAutocomplete.command.catchUpDescription': '맥락을 다시 파악합니다: 무엇을 하고 있었고 어디서 이어서 할지.', 'chat.commandAutocomplete.command.catchUpDescription': '맥락을 다시 파악합니다: 무엇을 하고 있었고 어디서 이어서 할지.',
'chat.commandAutocomplete.command.debugDescription': '수정안을 제시하기 전에 버그의 근본 원인을 단계적으로 조사합니다.', 'chat.commandAutocomplete.command.debugDescription': '수정안을 제시하기 전에 버그의 근본 원인을 단계적으로 조사합니다.',
'chat.commandAutocomplete.command.weighDescription': '결정하기 전에 2~3가지 접근 방식을 장단점과 함께 비교하고 추천을 제시합니다.', 'chat.commandAutocomplete.command.weighDescription': '결정하기 전에 2~3가지 접근 방식을 장단점과 함께 비교하고 추천을 제시합니다.',
@@ -833,6 +833,9 @@ export const settingsDict = {
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'Pokazuj powiadomienia o aktualizacjach OpenCode', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': 'Pokazuj powiadomienia o aktualizacjach OpenCode',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'Pokazuj powiadomienia o aktualizacjach OpenCode', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': 'Pokazuj powiadomienia o aktualizacjach OpenCode',
'settings.openchamber.opencodeCli.field.agentControlTool': 'Narzędzie sterowania dla agentów',
'settings.openchamber.opencodeCli.field.agentControlToolAria': 'Włącz narzędzie sterowania dla agentów',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': 'Pozwól agentom orkiestrować Twoją pracę z czatu: tworzyć sesje i worktree, delegować prompty innym agentom oraz zarządzać zaplanowanymi zadaniami. Dodaje krótki opis narzędzia do każdej sesji. Zastosowane po Save + Reload.',
'settings.openchamber.opencodeCli.tipMiddle': 'zmienna środowiskowa, ale to ustawienie jest zapisywane w', 'settings.openchamber.opencodeCli.tipMiddle': 'zmienna środowiskowa, ale to ustawienie jest zapisywane w',
'settings.openchamber.opencodeCli.tipPrefix': 'Wskazówka: możesz również użyć', 'settings.openchamber.opencodeCli.tipPrefix': 'Wskazówka: możesz również użyć',
'settings.openchamber.opencodeCli.title': 'OpenCode CLI', 'settings.openchamber.opencodeCli.title': 'OpenCode CLI',
+3
View File
@@ -649,6 +649,7 @@ export const dict: Record<I18nKey, string> = {
'chat.draftPresets.weigh.label': 'Weigh my options', 'chat.draftPresets.weigh.label': 'Weigh my options',
'chat.draftPresets.plan.label': 'Start feature planning', 'chat.draftPresets.plan.label': 'Start feature planning',
'chat.draftPresets.craftGoal.label': 'Utwórz Goal', 'chat.draftPresets.craftGoal.label': 'Utwórz Goal',
'chat.draftPresets.scheduleTask.label': 'Zaplanuj zadanie',
'chat.draftPresets.debug.label': 'Debug an issue', 'chat.draftPresets.debug.label': 'Debug an issue',
'chat.draftPresets.review.label': 'Review my changes', 'chat.draftPresets.review.label': 'Review my changes',
'chat.draftStarters.add': 'Add a starter', 'chat.draftStarters.add': 'Add a starter',
@@ -724,7 +725,9 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.handoffReviewDescription': 'Utwórz lub użyj ponownie osobnej sesji przeglądu z wygenerowanego handoffu.', 'chat.commandAutocomplete.command.handoffReviewDescription': 'Utwórz lub użyj ponownie osobnej sesji przeglądu z wygenerowanego handoffu.',
'chat.commandAutocomplete.command.featurePlanDescription': 'Rozpocznij prowadzoną, interaktywną sesję planowania nowej funkcji.', 'chat.commandAutocomplete.command.featurePlanDescription': 'Rozpocznij prowadzoną, interaktywną sesję planowania nowej funkcji.',
'chat.commandAutocomplete.command.craftGoalDescription': 'Przekształć pomysł lub zadanie w jasny, weryfikowalny Goal.', 'chat.commandAutocomplete.command.craftGoalDescription': 'Przekształć pomysł lub zadanie w jasny, weryfikowalny Goal.',
'chat.commandAutocomplete.command.scheduleTaskDescription': 'Zdefiniuj zaplanowane zadanie w prowadzonym dialogu.',
'chat.chatInput.toast.craftGoalFailed': 'Nie udało się rozpocząć tworzenia Goal', 'chat.chatInput.toast.craftGoalFailed': 'Nie udało się rozpocząć tworzenia Goal',
'chat.chatInput.toast.scheduleTaskFailed': 'Nie udało się rozpocząć konfiguracji zaplanowanego zadania',
'chat.commandAutocomplete.command.catchUpDescription': 'Przywróć kontekst: nad czym pracowałeś i od czego kontynuować.', 'chat.commandAutocomplete.command.catchUpDescription': 'Przywróć kontekst: nad czym pracowałeś i od czego kontynuować.',
'chat.commandAutocomplete.command.debugDescription': 'Prowadzone badanie pierwotnej przyczyny błędu przed zaproponowaniem poprawki.', 'chat.commandAutocomplete.command.debugDescription': 'Prowadzone badanie pierwotnej przyczyny błędu przed zaproponowaniem poprawki.',
'chat.commandAutocomplete.command.weighDescription': 'Rozważ 2-3 podejścia z kompromisami i rekomendacją, zanim się zdecydujesz.', 'chat.commandAutocomplete.command.weighDescription': 'Rozważ 2-3 podejścia z kompromisami i rekomendacją, zanim się zdecydujesz.',
@@ -954,6 +954,9 @@ export const settingsDict = {
"settings.openchamber.opencodeCli.field.binaryPathPlaceholder": "/Users/you/.bun/bin/opencode", "settings.openchamber.opencodeCli.field.binaryPathPlaceholder": "/Users/you/.bun/bin/opencode",
"settings.openchamber.opencodeCli.field.showUpdateNotifications": "Mostrar notificações de atualização do OpenCode", "settings.openchamber.opencodeCli.field.showUpdateNotifications": "Mostrar notificações de atualização do OpenCode",
"settings.openchamber.opencodeCli.field.showUpdateNotificationsAria": "Mostrar notificações de atualização do OpenCode", "settings.openchamber.opencodeCli.field.showUpdateNotificationsAria": "Mostrar notificações de atualização do OpenCode",
"settings.openchamber.opencodeCli.field.agentControlTool": "Ferramenta de controle para agentes",
"settings.openchamber.opencodeCli.field.agentControlToolAria": "Ativar a ferramenta de controle para agentes",
"settings.openchamber.opencodeCli.field.agentControlToolInfo": "Permita que agentes orquestrem seu trabalho pelo chat: criar sessões e worktrees, delegar prompts a outros agentes e gerenciar tarefas agendadas. Adiciona uma pequena descrição de ferramenta a cada sessão. Aplicado após Save + Reload.",
"settings.openchamber.opencodeCli.actions.browseAria": "Buscar caminho do executável do OpenCode", "settings.openchamber.opencodeCli.actions.browseAria": "Buscar caminho do executável do OpenCode",
"settings.openchamber.opencodeCli.actions.browse": "Buscar", "settings.openchamber.opencodeCli.actions.browse": "Buscar",
"settings.openchamber.opencodeCli.actions.saveAndReload": "Salvar + Recarregar", "settings.openchamber.opencodeCli.actions.saveAndReload": "Salvar + Recarregar",
@@ -1737,6 +1737,7 @@ export const dict: Record<I18nKey, string> = {
"chat.draftPresets.weigh.label": "Weigh my options", "chat.draftPresets.weigh.label": "Weigh my options",
"chat.draftPresets.plan.label": "Start feature planning", "chat.draftPresets.plan.label": "Start feature planning",
"chat.draftPresets.craftGoal.label": "Criar um Goal", "chat.draftPresets.craftGoal.label": "Criar um Goal",
"chat.draftPresets.scheduleTask.label": "Agendar uma tarefa",
"chat.draftPresets.debug.label": "Debug an issue", "chat.draftPresets.debug.label": "Debug an issue",
"chat.draftPresets.review.label": "Review my changes", "chat.draftPresets.review.label": "Review my changes",
"chat.draftStarters.add": "Add a starter", "chat.draftStarters.add": "Add a starter",
@@ -1813,7 +1814,9 @@ export const dict: Record<I18nKey, string> = {
"chat.commandAutocomplete.command.handoffReviewDescription": "Crie ou reutilize uma sessão separada de revisão a partir de um handoff gerado.", "chat.commandAutocomplete.command.handoffReviewDescription": "Crie ou reutilize uma sessão separada de revisão a partir de um handoff gerado.",
"chat.commandAutocomplete.command.featurePlanDescription": "Inicie uma sessão de planejamento guiada e interativa para uma nova funcionalidade.", "chat.commandAutocomplete.command.featurePlanDescription": "Inicie uma sessão de planejamento guiada e interativa para uma nova funcionalidade.",
"chat.commandAutocomplete.command.craftGoalDescription": "Transforme uma ideia ou tarefa em um Goal claro e verificável.", "chat.commandAutocomplete.command.craftGoalDescription": "Transforme uma ideia ou tarefa em um Goal claro e verificável.",
"chat.commandAutocomplete.command.scheduleTaskDescription": "Defina uma tarefa agendada por meio de um diálogo guiado.",
"chat.chatInput.toast.craftGoalFailed": "Não foi possível iniciar a criação do Goal", "chat.chatInput.toast.craftGoalFailed": "Não foi possível iniciar a criação do Goal",
"chat.chatInput.toast.scheduleTaskFailed": "Não foi possível iniciar a configuração da tarefa agendada",
"chat.commandAutocomplete.command.catchUpDescription": "Retome o contexto: o que você estava fazendo e por onde continuar.", "chat.commandAutocomplete.command.catchUpDescription": "Retome o contexto: o que você estava fazendo e por onde continuar.",
"chat.commandAutocomplete.command.debugDescription": "Investigação guiada da causa raiz de um bug antes de propor uma correção.", "chat.commandAutocomplete.command.debugDescription": "Investigação guiada da causa raiz de um bug antes de propor uma correção.",
"chat.commandAutocomplete.command.weighDescription": "Compare 2-3 abordagens com seus prós e contras e uma recomendação antes de decidir.", "chat.commandAutocomplete.command.weighDescription": "Compare 2-3 abordagens com seus prós e contras e uma recomendação antes de decidir.",
@@ -954,6 +954,9 @@ export const settingsDict = {
"settings.openchamber.opencodeCli.field.binaryPathPlaceholder": "/Users/you/.bun/bin/opencode", "settings.openchamber.opencodeCli.field.binaryPathPlaceholder": "/Users/you/.bun/bin/opencode",
"settings.openchamber.opencodeCli.field.showUpdateNotifications": "Показувати сповіщення про оновлення OpenCode", "settings.openchamber.opencodeCli.field.showUpdateNotifications": "Показувати сповіщення про оновлення OpenCode",
"settings.openchamber.opencodeCli.field.showUpdateNotificationsAria": "Показувати сповіщення про оновлення OpenCode", "settings.openchamber.opencodeCli.field.showUpdateNotificationsAria": "Показувати сповіщення про оновлення OpenCode",
"settings.openchamber.opencodeCli.field.agentControlTool": "Інструмент керування для агентів",
"settings.openchamber.opencodeCli.field.agentControlToolAria": "Увімкнути інструмент керування для агентів",
"settings.openchamber.opencodeCli.field.agentControlToolInfo": "Дозвольте агентам оркеструвати вашу роботу з чату: створювати сесії та worktree, делегувати запити іншим агентам і керувати запланованими задачами. Додає невеликий опис інструмента до кожної сесії. Застосовується після Save + Reload.",
"settings.openchamber.opencodeCli.actions.browseAria": "Вибрати шлях до виконуваного файла OpenCode", "settings.openchamber.opencodeCli.actions.browseAria": "Вибрати шлях до виконуваного файла OpenCode",
"settings.openchamber.opencodeCli.actions.browse": "Огляд", "settings.openchamber.opencodeCli.actions.browse": "Огляд",
"settings.openchamber.opencodeCli.actions.saveAndReload": "Зберегти й перезавантажити", "settings.openchamber.opencodeCli.actions.saveAndReload": "Зберегти й перезавантажити",
+3
View File
@@ -1737,6 +1737,7 @@ export const dict: Record<I18nKey, string> = {
"chat.draftPresets.weigh.label": "Зважити варіанти", "chat.draftPresets.weigh.label": "Зважити варіанти",
"chat.draftPresets.plan.label": "Розпочати планування фічі", "chat.draftPresets.plan.label": "Розпочати планування фічі",
"chat.draftPresets.craftGoal.label": "Сформувати Goal", "chat.draftPresets.craftGoal.label": "Сформувати Goal",
"chat.draftPresets.scheduleTask.label": "Запланувати задачу",
"chat.draftPresets.debug.label": "Дебаг проблеми", "chat.draftPresets.debug.label": "Дебаг проблеми",
"chat.draftPresets.review.label": "Переглянути мої зміни", "chat.draftPresets.review.label": "Переглянути мої зміни",
"chat.draftStarters.add": "Додати стартер", "chat.draftStarters.add": "Додати стартер",
@@ -1813,7 +1814,9 @@ export const dict: Record<I18nKey, string> = {
"chat.commandAutocomplete.command.handoffReviewDescription": "Створити або повторно використати окрему сесію ревʼю зі згенерованого handoff.", "chat.commandAutocomplete.command.handoffReviewDescription": "Створити або повторно використати окрему сесію ревʼю зі згенерованого handoff.",
"chat.commandAutocomplete.command.featurePlanDescription": "Розпочати покрокову діалогову сесію планування нової фічі.", "chat.commandAutocomplete.command.featurePlanDescription": "Розпочати покрокову діалогову сесію планування нової фічі.",
"chat.commandAutocomplete.command.craftGoalDescription": "Перетворити ідею або завдання на чіткий Goal, який можна перевірити.", "chat.commandAutocomplete.command.craftGoalDescription": "Перетворити ідею або завдання на чіткий Goal, який можна перевірити.",
"chat.commandAutocomplete.command.scheduleTaskDescription": "Визначити заплановану задачу через керований діалог.",
"chat.chatInput.toast.craftGoalFailed": "Не вдалося розпочати формування Goal", "chat.chatInput.toast.craftGoalFailed": "Не вдалося розпочати формування Goal",
"chat.chatInput.toast.scheduleTaskFailed": "Не вдалося розпочати налаштування запланованої задачі",
"chat.commandAutocomplete.command.catchUpDescription": "Повернутись у контекст: над чим працювали і звідки продовжити.", "chat.commandAutocomplete.command.catchUpDescription": "Повернутись у контекст: над чим працювали і звідки продовжити.",
"chat.commandAutocomplete.command.debugDescription": "Кероване дослідження першопричини бага перед тим, як пропонувати фікс.", "chat.commandAutocomplete.command.debugDescription": "Кероване дослідження першопричини бага перед тим, як пропонувати фікс.",
"chat.commandAutocomplete.command.weighDescription": "Зважити 2-3 підходи з trade-offs і рекомендацією перш ніж братися до роботи.", "chat.commandAutocomplete.command.weighDescription": "Зважити 2-3 підходи з trade-offs і рекомендацією перш ніж братися до роботи.",
@@ -954,6 +954,9 @@ export const settingsDict = {
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': '显示 OpenCode 更新通知', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': '显示 OpenCode 更新通知',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': '显示 OpenCode 更新通知', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': '显示 OpenCode 更新通知',
'settings.openchamber.opencodeCli.field.agentControlTool': '智能体控制工具',
'settings.openchamber.opencodeCli.field.agentControlToolAria': '启用智能体控制工具',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': '让智能体在聊天中编排你的工作:创建会话和 worktree、将提示委派给其他智能体、管理计划任务。会为每个会话添加少量工具说明。在 Save + Reload 后生效。',
'settings.openchamber.opencodeCli.actions.browseAria': '浏览 OpenCode 可执行文件路径', 'settings.openchamber.opencodeCli.actions.browseAria': '浏览 OpenCode 可执行文件路径',
'settings.openchamber.opencodeCli.actions.browse': '浏览', 'settings.openchamber.opencodeCli.actions.browse': '浏览',
'settings.openchamber.opencodeCli.actions.saveAndReload': '保存并重载', 'settings.openchamber.opencodeCli.actions.saveAndReload': '保存并重载',
@@ -1725,6 +1725,7 @@ export const dict: Record<I18nKey, string> = {
'chat.draftPresets.weigh.label': 'Weigh my options', 'chat.draftPresets.weigh.label': 'Weigh my options',
'chat.draftPresets.plan.label': 'Start feature planning', 'chat.draftPresets.plan.label': 'Start feature planning',
'chat.draftPresets.craftGoal.label': '创建 Goal', 'chat.draftPresets.craftGoal.label': '创建 Goal',
'chat.draftPresets.scheduleTask.label': '安排计划任务',
'chat.draftPresets.debug.label': 'Debug an issue', 'chat.draftPresets.debug.label': 'Debug an issue',
'chat.draftPresets.review.label': 'Review my changes', 'chat.draftPresets.review.label': 'Review my changes',
'chat.draftStarters.add': 'Add a starter', 'chat.draftStarters.add': 'Add a starter',
@@ -1801,7 +1802,9 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.handoffReviewDescription': '根据生成的交接内容创建或复用独立的审查会话。', 'chat.commandAutocomplete.command.handoffReviewDescription': '根据生成的交接内容创建或复用独立的审查会话。',
'chat.commandAutocomplete.command.featurePlanDescription': '为新功能开始一次引导式的来回规划会话。', 'chat.commandAutocomplete.command.featurePlanDescription': '为新功能开始一次引导式的来回规划会话。',
'chat.commandAutocomplete.command.craftGoalDescription': '将想法或任务转化为清晰且可验证的 Goal。', 'chat.commandAutocomplete.command.craftGoalDescription': '将想法或任务转化为清晰且可验证的 Goal。',
'chat.commandAutocomplete.command.scheduleTaskDescription': '通过引导式对话定义计划任务。',
'chat.chatInput.toast.craftGoalFailed': '无法开始创建 Goal', 'chat.chatInput.toast.craftGoalFailed': '无法开始创建 Goal',
'chat.chatInput.toast.scheduleTaskFailed': '无法开始设置计划任务',
'chat.commandAutocomplete.command.catchUpDescription': '重新进入上下文:你之前在做什么、从哪里继续。', 'chat.commandAutocomplete.command.catchUpDescription': '重新进入上下文:你之前在做什么、从哪里继续。',
'chat.commandAutocomplete.command.debugDescription': '在提出修复方案前,引导式地排查 bug 的根本原因。', 'chat.commandAutocomplete.command.debugDescription': '在提出修复方案前,引导式地排查 bug 的根本原因。',
'chat.commandAutocomplete.command.weighDescription': '在动手前,权衡 2-3 种方案的利弊并给出推荐。', 'chat.commandAutocomplete.command.weighDescription': '在动手前,权衡 2-3 种方案的利弊并给出推荐。',
@@ -928,6 +928,9 @@
'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode', 'settings.openchamber.opencodeCli.field.binaryPathPlaceholder': '/Users/you/.bun/bin/opencode',
'settings.openchamber.opencodeCli.field.showUpdateNotifications': '顯示 OpenCode 更新通知', 'settings.openchamber.opencodeCli.field.showUpdateNotifications': '顯示 OpenCode 更新通知',
'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': '顯示 OpenCode 更新通知', 'settings.openchamber.opencodeCli.field.showUpdateNotificationsAria': '顯示 OpenCode 更新通知',
'settings.openchamber.opencodeCli.field.agentControlTool': '智慧代理控制工具',
'settings.openchamber.opencodeCli.field.agentControlToolAria': '啟用智慧代理控制工具',
'settings.openchamber.opencodeCli.field.agentControlToolInfo': '讓代理從聊天中協調你的工作:建立工作階段與 worktree、將提示委派給其他代理、管理排程任務。會為每個工作階段加入少量工具說明。在 Save + Reload 後生效。',
'settings.openchamber.opencodeCli.actions.browseAria': '瀏覽 OpenCode 可執行檔路徑', 'settings.openchamber.opencodeCli.actions.browseAria': '瀏覽 OpenCode 可執行檔路徑',
'settings.openchamber.opencodeCli.actions.browse': '瀏覽', 'settings.openchamber.opencodeCli.actions.browse': '瀏覽',
'settings.openchamber.opencodeCli.actions.saveAndReload': '儲存並重新載入', 'settings.openchamber.opencodeCli.actions.saveAndReload': '儲存並重新載入',
@@ -1729,6 +1729,7 @@ export const dict: Record<I18nKey, string> = {
'chat.draftPresets.weigh.label': 'Weigh my options', 'chat.draftPresets.weigh.label': 'Weigh my options',
'chat.draftPresets.plan.label': 'Start feature planning', 'chat.draftPresets.plan.label': 'Start feature planning',
'chat.draftPresets.craftGoal.label': '建立 Goal', 'chat.draftPresets.craftGoal.label': '建立 Goal',
'chat.draftPresets.scheduleTask.label': '排程任務',
'chat.draftPresets.debug.label': 'Debug an issue', 'chat.draftPresets.debug.label': 'Debug an issue',
'chat.draftPresets.review.label': 'Review my changes', 'chat.draftPresets.review.label': 'Review my changes',
'chat.draftStarters.add': 'Add a starter', 'chat.draftStarters.add': 'Add a starter',
@@ -1805,7 +1806,9 @@ export const dict: Record<I18nKey, string> = {
'chat.commandAutocomplete.command.handoffReviewDescription': '根據生成的交接內容建立或重用獨立的審查會話。', 'chat.commandAutocomplete.command.handoffReviewDescription': '根據生成的交接內容建立或重用獨立的審查會話。',
'chat.commandAutocomplete.command.featurePlanDescription': '為新功能開始一次引導式的來回規劃工作階段。', 'chat.commandAutocomplete.command.featurePlanDescription': '為新功能開始一次引導式的來回規劃工作階段。',
'chat.commandAutocomplete.command.craftGoalDescription': '將想法或任務轉化為清晰且可驗證的 Goal。', 'chat.commandAutocomplete.command.craftGoalDescription': '將想法或任務轉化為清晰且可驗證的 Goal。',
'chat.commandAutocomplete.command.scheduleTaskDescription': '透過引導式對話定義排程任務。',
'chat.chatInput.toast.craftGoalFailed': '無法開始建立 Goal', 'chat.chatInput.toast.craftGoalFailed': '無法開始建立 Goal',
'chat.chatInput.toast.scheduleTaskFailed': '無法開始設定排程任務',
'chat.commandAutocomplete.command.catchUpDescription': '重新進入上下文:你之前在做什麼、從哪裡繼續。', 'chat.commandAutocomplete.command.catchUpDescription': '重新進入上下文:你之前在做什麼、從哪裡繼續。',
'chat.commandAutocomplete.command.debugDescription': '在提出修復方案前,引導式地排查 bug 的根本原因。', 'chat.commandAutocomplete.command.debugDescription': '在提出修復方案前,引導式地排查 bug 的根本原因。',
'chat.commandAutocomplete.command.weighDescription': '在動手前,權衡 2-3 種方案的利弊並給出推薦。', 'chat.commandAutocomplete.command.weighDescription': '在動手前,權衡 2-3 種方案的利弊並給出推薦。',
+49 -6
View File
@@ -39,6 +39,8 @@ export type MagicPromptId =
| 'session.plan.instructions' | 'session.plan.instructions'
| 'session.craftGoal.visible' | 'session.craftGoal.visible'
| 'session.craftGoal.instructions' | 'session.craftGoal.instructions'
| 'session.scheduleTask.visible'
| 'session.scheduleTask.instructions'
| 'session.catchup.visible' | 'session.catchup.visible'
| 'session.catchup.instructions' | 'session.catchup.instructions'
| 'session.debug.visible' | 'session.debug.visible'
@@ -709,7 +711,7 @@ Please review the latest state again and report any remaining issues.
Run this as a dialogue, not a one-shot answer. Run this as a dialogue, not a one-shot answer.
Whenever you ask the user a question, use the \`question\` tool. Do not ask questions in plain assistant text. Use the \`question\` tool only for clarifying decisions that have a small set of concrete answer options you already know from the conversation or your investigation — choices like option A/B/C, scope boundaries, or edge-case behavior. Ask open-ended questions, including what the user wants in the first place, in plain assistant text. Never invent speculative options just to fit the question tool.
1. Understand before asking. Once the user describes the idea, first investigate the codebase yourself — read the relevant files, existing patterns, data flow, and constraints. Ground every question in what the code actually shows, not in assumptions. 1. Understand before asking. Once the user describes the idea, first investigate the codebase yourself — read the relevant files, existing patterns, data flow, and constraints. Ground every question in what the code actually shows, not in assumptions.
@@ -723,6 +725,8 @@ Whenever you ask the user a question, use the \`question\` tool. Do not ask ques
6. When everything is settled, produce the final implementation plan: a clear, ordered breakdown of the work, the files and areas affected, the decisions that were made (and why), known risks, and any remaining assumptions flagged explicitly. The plan must reflect the user's actual answers — never fill gaps with guesses. 6. When everything is settled, produce the final implementation plan: a clear, ordered breakdown of the work, the files and areas affected, the decisions that were made (and why), known risks, and any remaining assumptions flagged explicitly. The plan must reflect the user's actual answers — never fill gaps with guesses.
7. After presenting the plan, if the \`openchamber\` tool is available, offer to start a separate session yourself to implement it; do so only after the user explicitly confirms, and include the full plan in that session's prompt because the new session cannot see this conversation.
Respond in the same language the user uses.`, Respond in the same language the user uses.`,
}, },
{ {
@@ -746,9 +750,9 @@ A Goal is a persistent completion contract, not an implementation plan and not a
Run this as a guided dialogue, not a one-shot answer. Run this as a guided dialogue, not a one-shot answer.
Whenever you ask the user a question, use the \`question\` tool. Do not ask questions in plain assistant text. Use the \`question\` tool only for clarifying decisions that have a small set of concrete answer options you already know from the conversation or your investigation — choices like option A/B/C, scope boundaries, or edge-case behavior. Ask open-ended questions, including what the user wants in the first place, in plain assistant text. Never invent speculative options just to fit the question tool.
1. Start from the user's intent. If the visible message includes an initial idea, use it immediately. Otherwise ask what they want to accomplish. Do not ask them to formulate the Goal themselves. 1. Start from the user's intent. If the visible message includes an initial idea, use it immediately. Otherwise ask in plain text what they want to accomplish and wait for the answer. Do not ask them to formulate the Goal themselves.
2. Investigate before asking when context is available. For repository work, inspect relevant code, tests, scripts, documentation, and conventions when that would answer questions or expose constraints. Do not ask for information that can be determined reliably from the workspace. 2. Investigate before asking when context is available. For repository work, inspect relevant code, tests, scripts, documentation, and conventions when that would answer questions or expose constraints. Do not ask for information that can be determined reliably from the workspace.
@@ -790,7 +794,46 @@ Whenever you ask the user a question, use the \`question\` tool. Do not ask ques
The proposed Goal should normally be one compact paragraph. Keep enough operational detail to make completion auditable, but remove conversational history, rationale, repetition, and implementation details that are not part of the completion contract. The proposed Goal should normally be one compact paragraph. Keep enough operational detail to make completion auditable, but remove conversational history, rationale, repetition, and implementation details that are not part of the completion contract.
Do not activate, execute, or claim completion of the proposed Goal. End by inviting the user to revise it or use it in the Goal dialog. Do not activate, execute, or claim completion of the proposed Goal. End by inviting the user to revise it or use it in the Goal dialog. If the \`openchamber\` tool is available, also offer to start a new Goal session for it yourself; do so only after the user explicitly confirms.
Respond in the same language the user uses.`,
},
{
id: 'session.scheduleTask.visible',
title: 'Scheduled Task Visible Prompt',
group: 'Session',
description: 'Visible user message sent by the /schedule-task command.',
placeholders: [
{ key: 'idea_block', description: 'Optional initial automation idea supplied after the command.' },
],
template: `Help me set up a scheduled task.{{idea_block}}`,
},
{
id: 'session.scheduleTask.instructions',
title: 'Scheduled Task Instructions',
group: 'Session',
description: 'Hidden instructions attached to the /schedule-task command. Guides the dialogue that defines a scheduled task and optionally creates it through the openchamber tool.',
template: `The user wants to set up a scheduled task: a saved prompt that OpenChamber runs automatically on a schedule (daily, weekly, one time, or cron) in a chosen project, with a chosen model and optional Goal Mode.
Run this as a guided dialogue, not a one-shot answer.
Use the \`question\` tool only for clarifying decisions that have a small set of concrete answer options you already know from the conversation or your investigation — choices like option A/B/C, scope boundaries, or edge-case behavior. Ask open-ended questions, including what the user wants in the first place, in plain assistant text. Never invent speculative options just to fit the question tool.
1. Start from the user's intent. If the visible message includes an initial idea, use it immediately. Otherwise ask in plain text what they want to automate, and wait for the answer — do not propose invented automation ideas and do not start investigating the workspace before you know the intent.
2. Investigate before asking. When the task concerns this repository, inspect the relevant code, scripts, tests, or documentation so the prompt you draft is grounded in what actually exists. Do not ask for information the workspace can answer.
3. Resolve the task definition:
- Name: a short, recognizable task name.
- Prompt: the exact instruction the scheduled agent receives on every run. It must be fully self-contained — the scheduled session has no memory of this conversation, so include every path, command, and expectation it needs.
- Schedule: a daily time, weekly days plus a time, a one-time date plus a time, or a cron expression; include the timezone when it matters.
- Model in provider/model format. Mention agent, variant, or Goal Mode with a token budget only if the user brings them up.
4. Ask only necessary questions, in batches of at most 3. Prefer concrete, decision-oriented questions.
5. Do not perform the task's work in this session. The deliverable is the scheduled task definition.
6. When everything is settled, present the final task definition clearly. If the \`openchamber\` tool is available, offer to create the task yourself and, only after the user explicitly confirms, create it and report the result. If the tool is unavailable, present the definition so the user can add it in OpenChamber's scheduled tasks UI.
Respond in the same language the user uses.`, Respond in the same language the user uses.`,
}, },
@@ -843,7 +886,7 @@ Respond in the same language the user uses.`,
description: 'Hidden instructions attached to the /debug command. Runs a guided root-cause investigation before proposing a fix.', description: 'Hidden instructions attached to the /debug command. Runs a guided root-cause investigation before proposing a fix.',
template: `The user wants help debugging an issue. Drive this as a focused root-cause investigation — not a plan, and not an immediate fix. template: `The user wants help debugging an issue. Drive this as a focused root-cause investigation — not a plan, and not an immediate fix.
Whenever you ask the user a question, use the \`question\` tool. Do not ask questions in plain assistant text. Use the \`question\` tool only for clarifying decisions that have a small set of concrete answer options you already know from the conversation or your investigation — choices like option A/B/C, scope boundaries, or edge-case behavior. Ask open-ended questions, including what the user wants in the first place, in plain assistant text. Never invent speculative options just to fit the question tool.
1. Get the symptom. When the user describes the problem, capture exactly what is observed versus expected — error messages, stack traces, failing behavior, and when it started. If a key detail is missing to even begin, ask for it briefly. 1. Get the symptom. When the user describes the problem, capture exactly what is observed versus expected — error messages, stack traces, failing behavior, and when it started. If a key detail is missing to even begin, ask for it briefly.
@@ -873,7 +916,7 @@ Respond in the same language the user uses.`,
description: 'Hidden instructions attached to the /weigh command. Investigates the code, then compares distinct approaches with trade-offs and a recommendation — no plan, no code.', description: 'Hidden instructions attached to the /weigh command. Investigates the code, then compares distinct approaches with trade-offs and a recommendation — no plan, no code.',
template: `The user knows WHAT they want to do but not HOW to approach it. Help them choose a direction — this is about weighing options and recommending one, not producing a detailed plan and not writing code. template: `The user knows WHAT they want to do but not HOW to approach it. Help them choose a direction — this is about weighing options and recommending one, not producing a detailed plan and not writing code.
Whenever you ask the user a question, use the \`question\` tool. Do not ask questions in plain assistant text. Use the \`question\` tool only for clarifying decisions that have a small set of concrete answer options you already know from the conversation or your investigation — choices like option A/B/C, scope boundaries, or edge-case behavior. Ask open-ended questions, including what the user wants in the first place, in plain assistant text. Never invent speculative options just to fit the question tool.
First, investigate. Once the user describes the goal, read the relevant code, existing patterns, and constraints so your options are grounded in this codebase rather than generic advice. Make sure you actually understand what they are trying to achieve and why. Ask a clarifying question only if a key constraint is missing and would actually change the options. First, investigate. Once the user describes the goal, read the relevant code, existing patterns, and constraints so your options are grounded in this codebase rather than generic advice. Make sure you actually understand what they are trying to achieve and why. Ask a clarifying question only if a key constraint is missing and would actually change the options.
@@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
mock.module('./runtime-url', () => ({
getRuntimeUrlResolver: () => ({ sse: (path: string) => `http://runtime.test${path}` }),
}));
mock.module('./runtime-switch', () => ({
subscribeRuntimeEndpointChanged: () => () => undefined,
}));
class MockEventSource {
static CLOSED = 2;
static instances: MockEventSource[] = [];
readyState = 1;
onopen: (() => void) | null = null;
onmessage: ((event: { data: string }) => void) | null = null;
onerror: (() => void) | null = null;
constructor(public readonly url: string) {
MockEventSource.instances.push(this);
}
close() {
this.readyState = MockEventSource.CLOSED;
}
}
describe('openchamber events', () => {
beforeEach(() => {
MockEventSource.instances = [];
globalThis.window = {} as Window & typeof globalThis;
globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
});
afterEach(() => {
delete (globalThis as { window?: unknown }).window;
delete (globalThis as { EventSource?: unknown }).EventSource;
});
test('dispatches externally created session events', async () => {
const { subscribeOpenchamberEvents } = await import('./openchamberEvents');
const events: unknown[] = [];
const listener = (event: unknown) => events.push(event);
const unsubscribe = subscribeOpenchamberEvents(listener);
const source = MockEventSource.instances[0];
source.onmessage?.({
data: JSON.stringify({
type: 'openchamber:session-created',
properties: {
sessionId: 'ses_123',
directory: '/repo/worktrees/research',
projectId: 'project_1',
createdAt: 123,
promptDispatched: true,
dispatchedAsCommand: false,
},
}),
});
expect(events).toEqual([
{
type: 'session-created',
sessionId: 'ses_123',
directory: '/repo/worktrees/research',
projectId: 'project_1',
createdAt: 123,
promptDispatched: true,
dispatchedAsCommand: false,
},
]);
unsubscribe();
});
});
+51 -9
View File
@@ -10,7 +10,17 @@ type ScheduledTaskRanEvent = {
sessionId?: string; sessionId?: string;
}; };
type OpenChamberEvent = ScheduledTaskRanEvent; type SessionCreatedEvent = {
type: 'session-created';
sessionId: string;
directory: string;
projectId?: string;
createdAt: number;
promptDispatched: boolean;
dispatchedAsCommand: boolean;
};
type OpenChamberEvent = ScheduledTaskRanEvent | SessionCreatedEvent;
type Listener = (event: OpenChamberEvent) => void; type Listener = (event: OpenChamberEvent) => void;
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null;
@@ -80,6 +90,13 @@ const parseEnvelope = (raw: string): { type: string; properties: unknown } | nul
} }
}; };
const getEventProperties = (properties: unknown): Record<string, unknown> | null => {
if (!properties || typeof properties !== 'object') {
return null;
}
return properties as Record<string, unknown>;
};
const dispatchFromEnvelope = (envelope: { type: string; properties: unknown }) => { const dispatchFromEnvelope = (envelope: { type: string; properties: unknown }) => {
if (envelope.type === 'openchamber:event-stream-ready') { if (envelope.type === 'openchamber:event-stream-ready') {
reconnectAttempt = 0; reconnectAttempt = 0;
@@ -90,17 +107,40 @@ const dispatchFromEnvelope = (envelope: { type: string; properties: unknown }) =
return; return;
} }
if (envelope.type === 'openchamber:session-created') {
const properties = getEventProperties(envelope.properties);
const sessionId = typeof properties?.sessionId === 'string' ? properties.sessionId : '';
const directory = typeof properties?.directory === 'string' ? properties.directory : '';
if (!sessionId || !directory) {
return;
}
const nextEvent: SessionCreatedEvent = {
type: 'session-created',
sessionId,
directory,
createdAt: typeof properties?.createdAt === 'number' ? properties.createdAt : Date.now(),
promptDispatched: properties?.promptDispatched === true,
dispatchedAsCommand: properties?.dispatchedAsCommand === true,
...(typeof properties?.projectId === 'string' && properties.projectId.length > 0
? { projectId: properties.projectId }
: {}),
};
for (const listener of listeners) {
listener(nextEvent);
}
return;
}
if (envelope.type !== 'openchamber:scheduled-task-ran') { if (envelope.type !== 'openchamber:scheduled-task-ran') {
return; return;
} }
const parsed = envelope.properties && typeof envelope.properties === 'object' const properties = getEventProperties(envelope.properties);
? envelope.properties as Record<string, unknown> const projectId = typeof properties?.projectId === 'string' ? properties.projectId : '';
: null; const taskId = typeof properties?.taskId === 'string' ? properties.taskId : '';
const projectId = typeof parsed?.projectId === 'string' ? parsed.projectId : ''; const ranAt = typeof properties?.ranAt === 'number' ? properties.ranAt : Date.now();
const taskId = typeof parsed?.taskId === 'string' ? parsed.taskId : ''; const rawStatus = properties?.status;
const ranAt = typeof parsed?.ranAt === 'number' ? parsed.ranAt : Date.now();
const rawStatus = parsed?.status;
const status = rawStatus === 'running' || rawStatus === 'error' ? rawStatus : 'success'; const status = rawStatus === 'running' || rawStatus === 'error' ? rawStatus : 'success';
if (!projectId || !taskId) { if (!projectId || !taskId) {
return; return;
@@ -112,7 +152,9 @@ const dispatchFromEnvelope = (envelope: { type: string; properties: unknown }) =
taskId, taskId,
ranAt, ranAt,
status, status,
...(typeof parsed?.sessionId === 'string' && parsed.sessionId.length > 0 ? { sessionId: parsed.sessionId } : {}), ...(typeof properties?.sessionId === 'string' && properties.sessionId.length > 0
? { sessionId: properties.sessionId }
: {}),
}; };
for (const listener of listeners) { for (const listener of listeners) {
listener(nextEvent); listener(nextEvent);
+9 -9
View File
@@ -295,7 +295,7 @@ describe('updateDesktopSettings', () => {
switchRuntimeEndpoint({ apiBaseUrl: 'https://load-b.example', runtimeKey: 'load-b' }); switchRuntimeEndpoint({ apiBaseUrl: 'https://load-b.example', runtimeKey: 'load-b' });
registerSettingsApi(async () => ({}), async () => ({ registerSettingsApi(async () => ({}), async () => ({
settings: { terminalShell: 'fish', draftStartersCraftGoalAdded: true }, settings: { terminalShell: 'fish', draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true },
source: 'web', source: 'web',
})); }));
await syncDesktopSettings(); await syncDesktopSettings();
@@ -303,14 +303,14 @@ describe('updateDesktopSettings', () => {
switchRuntimeEndpoint({ apiBaseUrl: 'https://load-a.example', runtimeKey: 'load-a' }); switchRuntimeEndpoint({ apiBaseUrl: 'https://load-a.example', runtimeKey: 'load-a' });
registerSettingsApi(async () => ({}), async () => ({ registerSettingsApi(async () => ({}), async () => ({
settings: { terminalShell: 'bash', draftStartersCraftGoalAdded: true }, settings: { terminalShell: 'bash', draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true },
source: 'web', source: 'web',
})); }));
await syncDesktopSettings(); await syncDesktopSettings();
expect(useUIStore.getState().terminalShell).toBe('bash'); expect(useUIStore.getState().terminalShell).toBe('bash');
originalLoad.resolve({ originalLoad.resolve({
settings: { terminalShell: 'zsh', draftStartersCraftGoalAdded: true }, settings: { terminalShell: 'zsh', draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true },
source: 'web', source: 'web',
}); });
await firstSync; await firstSync;
@@ -326,7 +326,7 @@ describe('updateDesktopSettings', () => {
themeId: 'theme-a', themeId: 'theme-a',
directoryShowHidden: true, directoryShowHidden: true,
sttModel: 'model-a', sttModel: 'model-a',
draftStartersCraftGoalAdded: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
}, },
source: 'web', source: 'web',
})); }));
@@ -334,7 +334,7 @@ describe('updateDesktopSettings', () => {
switchRuntimeEndpoint({ apiBaseUrl: 'https://mirror-b.example', runtimeKey: 'mirror-b' }); switchRuntimeEndpoint({ apiBaseUrl: 'https://mirror-b.example', runtimeKey: 'mirror-b' });
registerSettingsApi(async () => ({}), async () => ({ registerSettingsApi(async () => ({}), async () => ({
settings: { draftStartersCraftGoalAdded: true }, settings: { draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true },
source: 'web', source: 'web',
})); }));
await syncDesktopSettings(); await syncDesktopSettings();
@@ -360,7 +360,7 @@ describe('updateDesktopSettings', () => {
favoriteModels: [{ providerID: 'anthropic', modelID: 'claude-sonnet-4' }], favoriteModels: [{ providerID: 'anthropic', modelID: 'claude-sonnet-4' }],
followUpBehavior: 'steer', followUpBehavior: 'steer',
draftStarters: [{ type: 'command', name: 'runtime-a' }], draftStarters: [{ type: 'command', name: 'runtime-a' }],
draftStartersCraftGoalAdded: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
}, },
source: 'web', source: 'web',
})); }));
@@ -374,7 +374,7 @@ describe('updateDesktopSettings', () => {
switchRuntimeEndpoint({ apiBaseUrl: 'https://preferences-b.example', runtimeKey: 'preferences-b' }); switchRuntimeEndpoint({ apiBaseUrl: 'https://preferences-b.example', runtimeKey: 'preferences-b' });
registerSettingsApi(async () => ({}), async () => ({ registerSettingsApi(async () => ({}), async () => ({
settings: { draftStartersCraftGoalAdded: true }, settings: { draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true },
source: 'web', source: 'web',
})); }));
await syncDesktopSettings(); await syncDesktopSettings();
@@ -408,7 +408,7 @@ describe('updateDesktopSettings', () => {
recentModels: [{ providerID: 'google', modelID: 'gemini-pro' }], recentModels: [{ providerID: 'google', modelID: 'gemini-pro' }],
recentAgents: ['build', 'plan'], recentAgents: ['build', 'plan'],
recentEfforts: { 'anthropic/claude-haiku-4': ['high', 'default'] }, recentEfforts: { 'anthropic/claude-haiku-4': ['high', 'default'] },
draftStartersCraftGoalAdded: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
} satisfies SettingsPayload; } satisfies SettingsPayload;
registerSettingsApi(async () => ({}), async () => ({ settings, source: 'web' })); registerSettingsApi(async () => ({}), async () => ({ settings, source: 'web' }));
@@ -463,7 +463,7 @@ describe('updateDesktopSettings', () => {
expect(saveCalls).toHaveLength(1); expect(saveCalls).toHaveLength(1);
expect(saveCalls[0]).toEqual({ expect(saveCalls[0]).toEqual({
draftStartersCraftGoalAdded: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
favoriteModels: [{ providerID: 'anthropic', modelID: 'claude-haiku-4' }], favoriteModels: [{ providerID: 'anthropic', modelID: 'claude-haiku-4' }],
hiddenModels: [{ providerID: 'openai', modelID: 'gpt-5' }], hiddenModels: [{ providerID: 'openai', modelID: 'gpt-5' }],
collapsedModelProviders: ['openai'], collapsedModelProviders: ['openai'],
+37 -6
View File
@@ -548,6 +548,7 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
maxLastMessageLength: defaults.maxLastMessageLength, maxLastMessageLength: defaults.maxLastMessageLength,
inputSpellcheckEnabled: defaults.inputSpellcheckEnabled, inputSpellcheckEnabled: defaults.inputSpellcheckEnabled,
showOpenCodeUpdateNotifications: defaults.showOpenCodeUpdateNotifications, showOpenCodeUpdateNotifications: defaults.showOpenCodeUpdateNotifications,
agentControlToolEnabled: defaults.agentControlToolEnabled,
showToolFileIcons: defaults.showToolFileIcons, showToolFileIcons: defaults.showToolFileIcons,
codeBlockLineWrap: defaults.codeBlockLineWrap, codeBlockLineWrap: defaults.codeBlockLineWrap,
showTurnChangedFiles: defaults.showTurnChangedFiles, showTurnChangedFiles: defaults.showTurnChangedFiles,
@@ -703,6 +704,12 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
) { ) {
store.setShowOpenCodeUpdateNotifications(settings.showOpenCodeUpdateNotifications); store.setShowOpenCodeUpdateNotifications(settings.showOpenCodeUpdateNotifications);
} }
if (
typeof settings.agentControlToolEnabled === 'boolean'
&& settings.agentControlToolEnabled !== store.agentControlToolEnabled
) {
store.setAgentControlToolEnabled(settings.agentControlToolEnabled);
}
if (typeof settings.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) { if (typeof settings.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) {
store.setShowToolFileIcons(settings.showToolFileIcons); store.setShowToolFileIcons(settings.showToolFileIcons);
} }
@@ -804,17 +811,33 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
...nextStarters.slice(insertAt), ...nextStarters.slice(insertAt),
]; ];
} }
if (settings.draftStartersScheduleTaskAdded !== true && !nextStarters.some((starter) => starter.type === 'command' && starter.name === 'schedule-task')) {
const goalIndex = nextStarters.findIndex((starter) => starter.type === 'command' && starter.name === 'craft-goal');
const insertAt = goalIndex >= 0 ? goalIndex + 1 : nextStarters.length;
nextStarters = [
...nextStarters.slice(0, insertAt),
{ type: 'command', name: 'schedule-task' },
...nextStarters.slice(insertAt),
];
}
if (JSON.stringify(store.globalDraftStarters) !== JSON.stringify(nextStarters)) { if (JSON.stringify(store.globalDraftStarters) !== JSON.stringify(nextStarters)) {
store.setGlobalDraftStarters(nextStarters); store.setGlobalDraftStarters(nextStarters);
} }
if (settings.draftStartersCraftGoalAdded !== true) { if (settings.draftStartersCraftGoalAdded !== true || settings.draftStartersScheduleTaskAdded !== true) {
settings.draftStarters = nextStarters; settings.draftStarters = nextStarters;
settings.draftStartersCraftGoalAdded = true; settings.draftStartersCraftGoalAdded = true;
settings.draftStartersScheduleTaskAdded = true;
}
} else {
// The built-in default already contains Craft a Goal and Schedule a Task;
// only persist the markers so removing them later remains a durable user
// choice.
if (settings.draftStartersCraftGoalAdded !== true) {
settings.draftStartersCraftGoalAdded = true;
}
if (settings.draftStartersScheduleTaskAdded !== true) {
settings.draftStartersScheduleTaskAdded = true;
} }
} else if (settings.draftStartersCraftGoalAdded !== true) {
// The built-in default already contains Craft a Goal; only persist the marker
// so removing it later remains a durable user choice.
settings.draftStartersCraftGoalAdded = true;
} }
if (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) { if (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) {
store.setTerminalFontSize(settings.terminalFontSize); store.setTerminalFontSize(settings.terminalFontSize);
@@ -1023,6 +1046,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.draftStartersCraftGoalAdded === 'boolean') { if (typeof candidate.draftStartersCraftGoalAdded === 'boolean') {
result.draftStartersCraftGoalAdded = candidate.draftStartersCraftGoalAdded; result.draftStartersCraftGoalAdded = candidate.draftStartersCraftGoalAdded;
} }
if (typeof candidate.draftStartersScheduleTaskAdded === 'boolean') {
result.draftStartersScheduleTaskAdded = candidate.draftStartersScheduleTaskAdded;
}
if (typeof candidate.showReasoningTraces === 'boolean') { if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces; result.showReasoningTraces = candidate.showReasoningTraces;
} }
@@ -1302,6 +1328,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') { if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') {
result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications; result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications;
} }
if (typeof candidate.agentControlToolEnabled === 'boolean') {
result.agentControlToolEnabled = candidate.agentControlToolEnabled;
}
if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') { if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') {
result.openCodeUpdateToastDismissedVersion = candidate.openCodeUpdateToastDismissedVersion.trim().slice(0, 128); result.openCodeUpdateToastDismissedVersion = candidate.openCodeUpdateToastDismissedVersion.trim().slice(0, 128);
} }
@@ -1672,7 +1701,8 @@ export const syncDesktopSettings = async (): Promise<void> => {
// prevent server settings from reaching the Zustand store. // prevent server settings from reaching the Zustand store.
const applySettings = async (settings: DesktopSettings) => { const applySettings = async (settings: DesktopSettings) => {
if (!isSettingsRuntimeContextCurrent(context)) return; if (!isSettingsRuntimeContextCurrent(context)) return;
const shouldPersistCraftGoalMigration = settings.draftStartersCraftGoalAdded !== true; const shouldPersistCraftGoalMigration = settings.draftStartersCraftGoalAdded !== true
|| settings.draftStartersScheduleTaskAdded !== true;
const authoritativeSettings = materializeAuthoritativeUiSettings(settings); const authoritativeSettings = materializeAuthoritativeUiSettings(settings);
try { try {
persistToLocalStorage(settings); persistToLocalStorage(settings);
@@ -1693,6 +1723,7 @@ export const syncDesktopSettings = async (): Promise<void> => {
await updateDesktopSettings({ await updateDesktopSettings({
...(authoritativeSettings.draftStarters ? { draftStarters: authoritativeSettings.draftStarters } : {}), ...(authoritativeSettings.draftStarters ? { draftStarters: authoritativeSettings.draftStarters } : {}),
draftStartersCraftGoalAdded: true, draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
}); });
if (!isSettingsRuntimeContextCurrent(context)) return; if (!isSettingsRuntimeContextCurrent(context)) return;
} }
+8
View File
@@ -452,6 +452,14 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
keywords: ['opencode', 'cli', 'updates'], keywords: ['opencode', 'cli', 'updates'],
isAvailable: (ctx) => !ctx.isVSCode, isAvailable: (ctx) => !ctx.isVSCode,
}, },
{
id: 'sessions.agent-control-tool',
page: 'general',
titleKey: 'settings.openchamber.opencodeCli.field.agentControlTool',
descriptionKey: 'settings.openchamber.opencodeCli.field.agentControlToolInfo',
keywords: ['agent', 'tool', 'orchestration', 'openchamber', 'sessions', 'schedule', 'control'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{ {
id: 'git.github-account', id: 'git.github-account',
page: 'git', page: 'git',
+8 -1
View File
@@ -101,7 +101,7 @@ const TOOL_METADATA: Record<string, ToolMetadata> = {
] ]
}, },
task: { task: {
displayName: 'Agent Task', displayName: 'Agent Task',
category: 'ai', category: 'ai',
outputLanguage: 'markdown', outputLanguage: 'markdown',
@@ -187,6 +187,13 @@ const TOOL_METADATA: Record<string, ToolMetadata> = {
] ]
}, },
openchamber: {
displayName: 'OpenChamber',
category: 'system',
outputLanguage: 'json',
inputFields: []
},
plan_enter: { plan_enter: {
displayName: 'Plan Mode', displayName: 'Plan Mode',
category: 'ai', category: 'ai',
+7
View File
@@ -645,6 +645,7 @@ interface UIStore {
showTerminalQuickKeysOnDesktop: boolean; showTerminalQuickKeysOnDesktop: boolean;
persistChatDraft: boolean; persistChatDraft: boolean;
showOpenCodeUpdateNotifications: boolean; showOpenCodeUpdateNotifications: boolean;
agentControlToolEnabled: boolean;
inputSpellcheckEnabled: boolean; inputSpellcheckEnabled: boolean;
wideChatLayoutEnabled: boolean; wideChatLayoutEnabled: boolean;
codeBlockLineWrap: boolean; codeBlockLineWrap: boolean;
@@ -805,6 +806,7 @@ interface UIStore {
setMaxLastMessageLength: (value: number) => void; setMaxLastMessageLength: (value: number) => void;
setPersistChatDraft: (value: boolean) => void; setPersistChatDraft: (value: boolean) => void;
setShowOpenCodeUpdateNotifications: (value: boolean) => void; setShowOpenCodeUpdateNotifications: (value: boolean) => void;
setAgentControlToolEnabled: (value: boolean) => void;
setInputSpellcheckEnabled: (value: boolean) => void; setInputSpellcheckEnabled: (value: boolean) => void;
setWideChatLayoutEnabled: (value: boolean) => void; setWideChatLayoutEnabled: (value: boolean) => void;
setCodeBlockLineWrap: (value: boolean) => void; setCodeBlockLineWrap: (value: boolean) => void;
@@ -957,6 +959,7 @@ export const useUIStore = create<UIStore>()(
showTerminalQuickKeysOnDesktop: false, showTerminalQuickKeysOnDesktop: false,
persistChatDraft: true, persistChatDraft: true,
showOpenCodeUpdateNotifications: true, showOpenCodeUpdateNotifications: true,
agentControlToolEnabled: true,
inputSpellcheckEnabled: false, inputSpellcheckEnabled: false,
wideChatLayoutEnabled: false, wideChatLayoutEnabled: false,
codeBlockLineWrap: true, codeBlockLineWrap: true,
@@ -2105,6 +2108,9 @@ export const useUIStore = create<UIStore>()(
setShowOpenCodeUpdateNotifications: (value) => { setShowOpenCodeUpdateNotifications: (value) => {
set({ showOpenCodeUpdateNotifications: value }); set({ showOpenCodeUpdateNotifications: value });
}, },
setAgentControlToolEnabled: (value) => {
set({ agentControlToolEnabled: value });
},
setInputSpellcheckEnabled: (value) => { setInputSpellcheckEnabled: (value) => {
set({ inputSpellcheckEnabled: value }); set({ inputSpellcheckEnabled: value });
}, },
@@ -2395,6 +2401,7 @@ export const useUIStore = create<UIStore>()(
maxLastMessageLength: state.maxLastMessageLength, maxLastMessageLength: state.maxLastMessageLength,
persistChatDraft: state.persistChatDraft, persistChatDraft: state.persistChatDraft,
showOpenCodeUpdateNotifications: state.showOpenCodeUpdateNotifications, showOpenCodeUpdateNotifications: state.showOpenCodeUpdateNotifications,
agentControlToolEnabled: state.agentControlToolEnabled,
inputSpellcheckEnabled: state.inputSpellcheckEnabled, inputSpellcheckEnabled: state.inputSpellcheckEnabled,
wideChatLayoutEnabled: state.wideChatLayoutEnabled, wideChatLayoutEnabled: state.wideChatLayoutEnabled,
codeBlockLineWrap: state.codeBlockLineWrap, codeBlockLineWrap: state.codeBlockLineWrap,
+4
View File
@@ -49,6 +49,10 @@ openchamber update # Update to latest version
`startup enable` snapshots your current environment into the native service so startup behaves like you launched `openchamber` from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use `--no-env-snapshot` for a minimal service env. `startup enable` snapshots your current environment into the native service so startup behaves like you launched `openchamber` from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use `--no-env-snapshot` for a minimal service env.
When OpenChamber launches the local OpenCode server, it also registers a native
`openchamber` agent tool for project, session, and scheduled-task orchestration.
The tool is not injected when connecting to an external OpenCode server.
### Tunnel behavior notes ### Tunnel behavior notes
- One active tunnel per running OpenChamber instance (port). - One active tunnel per running OpenChamber instance (port).
+53 -2
View File
@@ -22,6 +22,7 @@ import {
import { import {
parseArgs, parseArgs,
showHelp, showHelp,
showControlHelp,
showStartupHelp, showStartupHelp,
showConnectUrlHelp, showConnectUrlHelp,
showTunnelHelp, showTunnelHelp,
@@ -33,6 +34,10 @@ import { resolveExplicitBinary, searchPathFor } from './lib/cli-executables.js';
import { startupCommand } from './lib/commands-startup.js'; import { startupCommand } from './lib/commands-startup.js';
import { logsCommand } from './lib/commands-logs.js'; import { logsCommand } from './lib/commands-logs.js';
import { statusCommand } from './lib/commands-status.js'; import { statusCommand } from './lib/commands-status.js';
import { scheduleCommand } from './lib/commands-schedule.js';
import { sessionCommand } from './lib/commands-session.js';
import { modelsCommand } from './lib/commands-models.js';
import { projectsCommand } from './lib/commands-projects.js';
import { createUpdateCommand } from './lib/commands-update.js'; import { createUpdateCommand } from './lib/commands-update.js';
import { createConnectUrlCommand } from './lib/commands-connect-url.js'; import { createConnectUrlCommand } from './lib/commands-connect-url.js';
import { createLifecycleCommands } from './lib/commands-lifecycle.js'; import { createLifecycleCommands } from './lib/commands-lifecycle.js';
@@ -176,6 +181,13 @@ const commands = {
status: statusCommand, status: statusCommand,
schedule: scheduleCommand,
session: sessionCommand,
models: modelsCommand,
projects: projectsCommand,
logs: logsCommand, logs: logsCommand,
@@ -219,7 +231,7 @@ commands.update = createUpdateCommand({
async function main() { async function main() {
const parsed = parseArgs(); const parsed = parseArgs();
const { command, subcommand, tunnelAction, startupAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed; const { command, subcommand, tunnelAction, startupAction, scheduleAction, sessionAction, controlAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed;
activeCommandOptions = options; activeCommandOptions = options;
if (versionRequested) { if (versionRequested) {
@@ -255,6 +267,16 @@ async function main() {
showStartupHelp(); showStartupHelp();
} else if (command === 'connect-url') { } else if (command === 'connect-url') {
showConnectUrlHelp(); showConnectUrlHelp();
} else if (command === 'schedule') {
await commands.schedule(options, 'help');
} else if (command === 'session') {
await commands.session(options, 'help');
} else if (command === 'models') {
await commands.models(options, 'help');
} else if (command === 'projects') {
await commands.projects(options, 'help');
} else if (command === 'control') {
showControlHelp();
} else { } else {
showHelp(); showHelp();
} }
@@ -271,8 +293,36 @@ async function main() {
return; return;
} }
if (command === 'schedule') {
await commands.schedule(options, scheduleAction);
return;
}
if (command === 'session') {
await commands.session(options, sessionAction);
return;
}
if (command === 'models') {
await commands.models(options, 'show');
return;
}
if (command === 'projects') {
await commands.projects(options, 'list');
return;
}
if (command === 'control') {
if (controlAction !== 'help') {
throw new TunnelCliError(`Unknown control command '${controlAction}'.`, EXIT_CODE.USAGE_ERROR);
}
showControlHelp();
return;
}
if (!commands[command]) { if (!commands[command]) {
const knownCommands = ['serve', 'stop', 'restart', 'status', 'tunnel', 'startup', 'logs', 'update']; const knownCommands = ['serve', 'stop', 'restart', 'status', 'schedule', 'session', 'models', 'projects', 'control', 'tunnel', 'startup', 'logs', 'update'];
const suggestion = findClosestMatch(command, knownCommands); const suggestion = findClosestMatch(command, knownCommands);
const hint = suggestion ? ` Did you mean '${suggestion}'?` : ''; const hint = suggestion ? ` Did you mean '${suggestion}'?` : '';
if (isJsonMode(options)) { if (isJsonMode(options)) {
@@ -372,6 +422,7 @@ if (isCliExecution) {
} }
export { export {
main,
commands, commands,
parseArgs, parseArgs,
assertAuthenticatedNetworkExposure, assertAuthenticatedNetworkExposure,
+491 -1
View File
@@ -9,7 +9,18 @@ import { pathToFileURL } from 'url';
import { isModuleCliExecution, normalizeCliEntryPath } from './cli-entry.js'; import { isModuleCliExecution, normalizeCliEntryPath } from './cli-entry.js';
import { requestJson } from './lib/cli-http.js'; import { requestJson } from './lib/cli-http.js';
import { requestControlAction } from './lib/cli-control.js';
import { inspectTunnelAttachability } from './lib/cli-lifecycle.js'; import { inspectTunnelAttachability } from './lib/cli-lifecycle.js';
import { formatGoal } from './lib/commands-schedule.js';
import {
buildSessionCreatePayload,
buildSessionPromptPayload,
formatSessionLine,
sessionCommand,
} from './lib/commands-session.js';
import { formatModelsOutput } from './lib/commands-models.js';
import { formatProjectLine } from './lib/commands-projects.js';
import { resolveTargetPort } from './lib/cli-api-target.js';
import { DEFAULT_TUNNEL_PROVIDER_CAPABILITIES } from './lib/cli-tunnel-capabilities.js'; import { DEFAULT_TUNNEL_PROVIDER_CAPABILITIES } from './lib/cli-tunnel-capabilities.js';
import { import {
TUNNEL_PROVIDER_CLOUDFLARE, TUNNEL_PROVIDER_CLOUDFLARE,
@@ -229,6 +240,351 @@ describe('cli args', () => {
expect(parsed.options.port).toBe(3002); expect(parsed.options.port).toBe(3002);
}); });
it('parses schedule commands and options', () => {
const parsed = parseArgs([
'schedule',
'create',
'--project',
'proj_1',
'--name',
'Daily review',
'--prompt',
'Review the repo',
'--model',
'openai/gpt-5.5',
'--daily',
'09:30',
'--timezone',
'Europe/Kyiv',
]);
expect(parsed.command).toBe('schedule');
expect(parsed.scheduleAction).toBe('create');
expect(parsed.options.project).toBe('proj_1');
expect(parsed.options.name).toBe('Daily review');
expect(parsed.options.prompt).toBe('Review the repo');
expect(parsed.options.model).toBe('openai/gpt-5.5');
expect(parsed.options.daily).toBe('09:30');
expect(parsed.options.timezone).toBe('Europe/Kyiv');
});
it('parses goal-enabled scheduled task options', () => {
const parsed = parseArgs([
'schedule',
'create',
'--dir',
'/repo',
'--name',
'Finish migration',
'--prompt',
'Complete and verify the migration',
'--model',
'openai/gpt-5.5',
'--daily',
'09:30',
'--goal',
'--goal-token-budget',
'200000',
]);
expect(parsed.options.goal).toBe(true);
expect(parsed.options.goalTokenBudget).toBe('200000');
});
it('formats scheduled goal state compactly', () => {
expect(formatGoal({})).toBe('goal:no');
expect(formatGoal({ goalEnabled: true })).toBe('goal:yes');
expect(formatGoal({ goalEnabled: true, goalTokenBudget: 200000 })).toBe('goal:yes budget:200000');
});
it('parses session create options', () => {
const parsed = parseArgs([
'session',
'create',
'--dir',
'.',
'--name',
'Side task',
'--prompt',
'Investigate cache invalidation',
'--model',
'openai/gpt-5.5',
'--worktree',
'side-task',
'--branch',
'openchamber/side-task',
'--base',
'main',
'--no-upstream',
]);
expect(parsed.command).toBe('session');
expect(parsed.sessionAction).toBe('create');
expect(parsed.options.directory).toBe('.');
expect(parsed.options.name).toBe('Side task');
expect(parsed.options.prompt).toBe('Investigate cache invalidation');
expect(parsed.options.model).toBe('openai/gpt-5.5');
expect(parsed.options.worktree).toBe('side-task');
expect(parsed.options.branch).toBe('openchamber/side-task');
expect(parsed.options.startRef).toBe('main');
expect(parsed.options.setUpstream).toBe(false);
});
it('parses control help command', () => {
const parsed = parseArgs(['control', 'help']);
expect(parsed.command).toBe('control');
expect(parsed.controlAction).toBe('help');
});
it('builds session create payloads from CLI options', () => {
expect(buildSessionCreatePayload({
directory: '.',
name: 'Side task',
prompt: 'Investigate cache invalidation',
model: 'openai/gpt-5.5',
agent: 'build',
worktree: 'side-task',
branch: 'openchamber/side-task',
startRef: 'main',
setUpstream: true,
})).toEqual({
directory: '.',
title: 'Side task',
worktree: {
name: 'side-task',
branchName: 'openchamber/side-task',
startRef: 'main',
},
prompt: 'Investigate cache invalidation',
model: 'openai/gpt-5.5',
agent: 'build',
setUpstream: true,
});
});
it('allows session create prompts without an explicit model', () => {
expect(buildSessionCreatePayload({
directory: '.',
prompt: 'Investigate cache invalidation',
})).toEqual({
directory: '.',
prompt: 'Investigate cache invalidation',
});
});
it('builds goal-enabled session create payloads', () => {
const parsed = parseArgs([
'session',
'create',
'--dir',
'/repo',
'--prompt',
'Finish and verify the migration',
'--goal',
'--goal-token-budget',
'200000',
]);
expect(buildSessionCreatePayload(parsed.options)).toEqual({
directory: '/repo',
prompt: 'Finish and verify the migration',
goal: true,
goalTokenBudget: 200000,
});
});
it('validates session goal options before HTTP', () => {
expect(() => buildSessionCreatePayload({ directory: '/repo', goal: true })).toThrow('--goal requires --prompt.');
expect(() => buildSessionCreatePayload({
directory: '/repo',
prompt: 'Run',
goalTokenBudget: '200000',
})).toThrow('--goal-token-budget requires --goal.');
for (const value of ['999', '1.5', '100000001', 'nope']) {
expect(() => buildSessionCreatePayload({
directory: '/repo',
prompt: 'Run',
goal: true,
goalTokenBudget: value,
})).toThrow('--goal-token-budget must be an integer from 1000 to 100000000.');
}
});
it('parses session list filters', () => {
const parsed = parseArgs(['session', 'list', '--dir', '/repo', '--limit', '5']);
expect(parsed.command).toBe('session');
expect(parsed.sessionAction).toBe('list');
expect(parsed.options.directory).toBe('/repo');
expect(parsed.options.limit).toBe(5);
});
it('parses session status and message options', () => {
const status = parseArgs(['session', 'status', '--session', 'ses_123', '--dir', '/repo']);
expect(status.sessionAction).toBe('status');
expect(status.options.session).toBe('ses_123');
expect(status.options.directory).toBe('/repo');
const messages = parseArgs([
'session',
'messages',
'--session',
'ses_123',
'--dir',
'/repo',
'--last',
'--role',
'assistant',
]);
expect(messages.sessionAction).toBe('messages');
expect(messages.options.last).toBe(true);
expect(messages.options.role).toBe('assistant');
const waiting = parseArgs([
'session',
'messages',
'--session',
'ses_123',
'--dir',
'/repo',
'--wait',
'--timeout',
'30',
'--last-assistant',
]);
expect(waiting.options.wait).toBe(true);
expect(waiting.options.timeout).toBe('30');
expect(waiting.options.lastAssistant).toBe(true);
const list = parseArgs(['session', 'list', '--dir', '/repo', '--with-status']);
expect(list.options.withStatus).toBe(true);
});
it('parses session send and fork actions', () => {
const send = parseArgs([
'session', 'send', '--session', 'ses_123', '--dir', '/repo', '--prompt', 'Continue',
'--goal', '--wait', '--last-assistant',
]);
expect(send.sessionAction).toBe('send');
expect(send.options).toMatchObject({
session: 'ses_123',
directory: '/repo',
prompt: 'Continue',
goal: true,
wait: true,
lastAssistant: true,
});
const fork = parseArgs([
'session', 'fork', '--session', 'ses_123', '--dir', '/repo', '--message', 'msg_123',
'--prompt', 'Try another approach',
]);
expect(fork.sessionAction).toBe('fork');
expect(fork.options.message).toBe('msg_123');
});
it('builds session send and fork prompt payloads', () => {
expect(buildSessionPromptPayload({
session: 'ses_123',
directory: '/repo',
prompt: 'Continue',
model: 'openai/gpt-5.5',
agent: 'build',
goal: true,
goalTokenBudget: '200000',
}, 'send')).toEqual({
directory: '/repo',
prompt: 'Continue',
model: 'openai/gpt-5.5',
agent: 'build',
goal: true,
goalTokenBudget: 200000,
});
expect(buildSessionPromptPayload({
session: 'ses_123',
directory: '/repo',
message: 'msg_123',
prompt: 'Try another approach',
}, 'fork')).toEqual({
directory: '/repo',
messageId: 'msg_123',
prompt: 'Try another approach',
});
});
it('validates session message selectors before HTTP', async () => {
await expect(sessionCommand({ session: 'ses_123', directory: '/repo', all: true, last: true }, 'messages'))
.rejects.toThrow('--all cannot be combined with --last or --limit.');
await expect(sessionCommand({ session: 'ses_123', directory: '/repo', role: 'tool' }, 'messages'))
.rejects.toThrow('--role must be one of: all, user, assistant.');
await expect(sessionCommand({ session: 'ses_123' }, 'status'))
.rejects.toThrow('Missing required --dir.');
await expect(sessionCommand({ session: 'ses_123', directory: '/repo', timeout: '30' }, 'messages'))
.rejects.toThrow('--timeout requires --wait.');
await expect(sessionCommand({ directory: '/repo', lastAssistant: true }, 'create'))
.rejects.toThrow('--last-assistant requires --wait for session create.');
await expect(sessionCommand({ directory: '/repo', timeout: '30' }, 'create'))
.rejects.toThrow('--timeout requires --wait.');
await expect(sessionCommand({ session: 'ses_123', directory: '/repo' }, 'send'))
.rejects.toThrow('Missing required --prompt.');
await expect(sessionCommand({ session: 'ses_123', directory: '/repo', prompt: 'Run', message: 'msg_1' }, 'send'))
.rejects.toThrow('--message is only valid for session fork.');
await expect(sessionCommand({ session: 'ses_123', directory: '/repo', prompt: 'Run', lastAssistant: true }, 'fork'))
.rejects.toThrow('--last-assistant requires --wait for session fork.');
});
it('parses models command', () => {
const parsed = parseArgs(['models', '--json']);
expect(parsed.command).toBe('models');
expect(parsed.options.json).toBe(true);
});
it('parses projects command', () => {
const parsed = parseArgs(['projects', '--json']);
expect(parsed.command).toBe('projects');
expect(parsed.options.json).toBe(true);
});
it('formats projects compactly', () => {
expect(formatProjectLine({
id: 'path_repo',
label: 'Openchamber',
path: '/repo/openchamber',
})).toBe('- `Openchamber` — `path_repo` — `/repo/openchamber`');
});
it('formats model defaults and favorites compactly', () => {
expect(formatModelsOutput({
defaultModel: 'opencode-go/deepseek-v4-flash',
defaultAgent: 'build',
favoriteModels: [
{ providerID: 'openai', modelID: 'gpt-5.5' },
{ providerID: 'opencode-go', modelID: 'deepseek-v4-pro' },
],
recentModels: [
{ providerID: 'zai-coding-plan', modelID: 'glm-5.2' },
],
})).toBe('Default: `opencode-go/deepseek-v4-flash` / `build`\n\nFavorites:\n- `openai/gpt-5.5`\n- `opencode-go/deepseek-v4-pro`\n\nRecent:\n- `zai-coding-plan/glm-5.2`\n');
});
it('formats compact session list lines', () => {
expect(formatSessionLine({
title: 'Default model smoke test',
agent: 'build',
directory: '/repo',
model: { providerID: 'opencode-go', id: 'deepseek-v4-flash', variant: 'default' },
})).toBe('- `Default model smoke test` — `opencode-go/deepseek-v4-flash`, `build` — `/repo`');
expect(formatSessionLine({
title: 'Working session',
agent: 'build',
directory: '/repo',
model: { providerID: 'openai', id: 'gpt-5.4-mini' },
status: { type: 'busy' },
})).toContain('status:busy');
});
it('parses tunnel auto-start server options', () => { it('parses tunnel auto-start server options', () => {
const parsed = parseArgs(['tunnel', 'start', '--port', '3002', '--api-only', '--lan', '--ui-password', 'secret']); const parsed = parseArgs(['tunnel', 'start', '--port', '3002', '--api-only', '--lan', '--ui-password', 'secret']);
@@ -261,6 +617,50 @@ describe('cli args', () => {
}); });
}); });
describe('cli API target resolution', () => {
it('uses an explicit port without discovery', async () => {
await expect(resolveTargetPort(
{ explicitPort: true, port: 4567 },
{
discoverDesktopInstance: async () => { throw new Error('should not discover desktop'); },
discoverLifecycleInstances: async () => { throw new Error('should not discover lifecycle'); },
},
)).resolves.toBe(4567);
});
it('prefers a desktop instance when no port is explicit', async () => {
await expect(resolveTargetPort({}, {
discoverDesktopInstance: async () => ({ port: 4500 }),
discoverLifecycleInstances: async () => [{ port: 3001 }],
isServerHealthReady: async () => false,
})).resolves.toBe(4500);
});
it('uses the only discovered lifecycle instance', async () => {
await expect(resolveTargetPort({}, {
discoverDesktopInstance: async () => null,
discoverLifecycleInstances: async () => [{ port: 3002 }],
isServerHealthReady: async () => false,
})).resolves.toBe(3002);
});
it('uses healthy default port when discovery finds no instances', async () => {
await expect(resolveTargetPort({}, {
discoverDesktopInstance: async () => null,
discoverLifecycleInstances: async () => [],
isServerHealthReady: async (port) => port === 3000,
})).resolves.toBe(3000);
});
it('fails when multiple non-default instances are running', async () => {
await expect(resolveTargetPort({}, {
discoverDesktopInstance: async () => null,
discoverLifecycleInstances: async () => [{ port: 3001 }, { port: 3002 }],
isServerHealthReady: async () => false,
})).rejects.toThrow('Multiple OpenChamber instances are running');
});
});
describe('network-exposed auth validation', () => { describe('network-exposed auth validation', () => {
it('allows loopback without a UI password', () => { it('allows loopback without a UI password', () => {
expect(() => assertAuthenticatedNetworkExposure({ host: '127.0.0.1' })).not.toThrow(); expect(() => assertAuthenticatedNetworkExposure({ host: '127.0.0.1' })).not.toThrow();
@@ -333,8 +733,9 @@ describe('compatibility exports', () => {
it('includes ngrok in fallback tunnel providers when no server is reachable', async () => { it('includes ngrok in fallback tunnel providers when no server is reachable', async () => {
await withTempOpenChamberDataDir(async () => { await withTempOpenChamberDataDir(async () => {
const port = await allocateLoopbackPort();
const output = await captureStdout(async () => { const output = await captureStdout(async () => {
await commands.tunnel({ json: true }, 'providers'); await commands.tunnel({ json: true, explicitPort: true, port }, 'providers');
}); });
const body = JSON.parse(output); const body = JSON.parse(output);
@@ -368,6 +769,27 @@ describe('compatibility exports', () => {
}); });
describe('CLI HTTP helpers', () => { describe('CLI HTTP helpers', () => {
it('sends one typed request to the shared control endpoint', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
expect(new URL(String(url)).pathname).toBe('/api/openchamber/control');
expect(options.method).toBe('POST');
expect(JSON.parse(options.body)).toEqual({
action: 'session.status',
input: { sessionId: 'ses_1', directory: '/repo' },
});
return createMockJsonResponse({ status: 'ok', sessionStatus: { type: 'idle' } });
};
try {
await expect(requestControlAction(45677, 'session.status', {
sessionId: 'ses_1',
directory: '/repo',
})).resolves.toEqual({ status: 'ok', sessionStatus: { type: 'idle' } });
} finally {
globalThis.fetch = originalFetch;
}
});
it('retries UI-authenticated API requests with the stored instance password', async () => { it('retries UI-authenticated API requests with the stored instance password', async () => {
await withTempOpenChamberDataDir(async () => { await withTempOpenChamberDataDir(async () => {
const port = 45678; const port = 45678;
@@ -412,6 +834,74 @@ describe('CLI HTTP helpers', () => {
} }
}); });
}); });
it('prefers the stored instance password over a non-explicit env password', async () => {
await withTempOpenChamberDataDir(async () => {
const port = 45679;
fs.writeFileSync(await getInstanceFilePath(port), JSON.stringify({ port, uiPassword: 'stored-secret' }, null, 2));
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
if (String(url).endsWith('/auth/session')) {
expect(JSON.parse(options.body)).toEqual({ password: 'stored-secret' });
return {
ok: true,
headers: { getSetCookie: () => ['oc_ui_session=session-token; Path=/; HttpOnly'] },
json: async () => ({ authenticated: true }),
};
}
if (options.headers?.Cookie === 'oc_ui_session=session-token') {
return createMockJsonResponse({ ok: true });
}
return {
ok: false,
status: 401,
json: async () => ({ error: 'UI authentication required', locked: true }),
};
};
try {
const { response, body } = await requestJson(port, '/api/openchamber/scheduled-tasks/status', {
uiPassword: 'stale-env-secret',
explicitUiPassword: false,
});
expect(response.ok).toBe(true);
expect(body).toEqual({ ok: true });
} finally {
globalThis.fetch = originalFetch;
}
});
});
it('authenticates desktop-local API requests with the stored client token', async () => {
await withTempOpenChamberDataDir(async (dir) => {
const port = 57123;
fs.writeFileSync(path.join(dir, 'settings.json'), JSON.stringify({
desktopLocalPort: port,
desktopLocalClientToken: 'oc_client_test',
}, null, 2));
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_url, options = {}) => {
if (options.headers?.Authorization === 'Bearer oc_client_test') {
return createMockJsonResponse({ ok: true });
}
return {
ok: false,
status: 401,
json: async () => ({ error: 'Client authentication required', locked: true }),
};
};
try {
const { response, body } = await requestJson(port, '/api/openchamber/scheduled-tasks/status');
expect(response.ok).toBe(true);
expect(body).toEqual({ ok: true });
} finally {
globalThis.fetch = originalFetch;
}
});
});
}); });
describe('cli entry detection', () => { describe('cli entry detection', () => {
+27
View File
@@ -25,6 +25,22 @@ Command modules implement user-facing commands and preserve output contracts acr
- Implements `openchamber status`. - Implements `openchamber status`.
- Formats discovered instances and tunnel readiness/status for human, quiet, and JSON output. - Formats discovered instances and tunnel readiness/status for human, quiet, and JSON output.
- `commands-session.js`
- Implements `openchamber session create`, `send`, `fork`, `list`, `status`, and `messages`.
- Maps CLI options to shared control-service inputs and owns only human, quiet, and JSON presentation.
- Message projection matches Export Markdown semantics: only ordered `text` parts are exposed; tool, reasoning, file, and other parts are omitted.
- The server control service owns create/worktree/prompt orchestration, official OpenCode reads, Goal Mode, wait semantics, and partial failures.
- `commands-schedule.js`
- Implements scheduled task status/list/create/run/delete/enable/disable.
- Maps options to control-service inputs and renders results; project resolution, validation, persistence, and execution remain server-owned.
- `commands-models.js`
- Prints OpenChamber default, favorite, and recent model settings.
- `commands-projects.js`
- Prints configured project labels, ids, and directories for later control-plane calls.
- `commands-logs.js` - `commands-logs.js`
- Implements `openchamber logs`. - Implements `openchamber logs`.
- Resolves log files, tails recent lines, and follows log output. - Resolves log files, tails recent lines, and follows log output.
@@ -70,6 +86,17 @@ These modules hold reusable, non-presentational logic for commands.
- `cli-http.js` - `cli-http.js`
- HTTP helpers for health checks, shutdown requests, JSON API calls, tunnel provider fetches, and system info fetches. - HTTP helpers for health checks, shutdown requests, JSON API calls, tunnel provider fetches, and system info fetches.
- Owns local desktop bearer auth and managed CLI-instance UI password retry for control-plane requests.
- `cli-control.js`
- Sends one typed action request to the authenticated OpenChamber control endpoint and maps HTTP failures to CLI exit behavior.
- Must not reproduce session, scheduled-task, project-resolution, or wait orchestration.
- `cli-api-target.js`
- Resolves the target OpenChamber runtime for control-plane commands, preferring desktop unless a port is explicit.
- `cli-goal.js`
- Owns shared Goal Mode token-budget validation for session and schedule commands.
- `cli-network.js` - `cli-network.js`
- Host resolution, URL building, LAN detection, unsafe browser port validation, and UI password/network exposure checks. - Host resolution, URL building, LAN detection, unsafe browser port validation, and UI password/network exposure checks.
+74
View File
@@ -0,0 +1,74 @@
import { DEFAULT_PORT } from './cli-args.js';
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
import { discoverDesktopInstance, discoverLifecycleInstances } from './cli-lifecycle.js';
import { isServerHealthReady } from './cli-http.js';
const uniquePorts = (entries) => {
const seen = new Set();
const ports = [];
for (const entry of entries) {
const port = typeof entry === 'number' ? entry : entry?.port;
if (!Number.isFinite(port) || port <= 0 || seen.has(port)) continue;
seen.add(port);
ports.push(port);
}
return ports;
};
async function resolveTargetPort(options = {}, deps = {}) {
if (options.explicitPort && Number.isFinite(options.port) && options.port > 0) {
return options.port;
}
const discoverDesktop = typeof deps.discoverDesktopInstance === 'function'
? deps.discoverDesktopInstance
: discoverDesktopInstance;
const discoverLifecycle = typeof deps.discoverLifecycleInstances === 'function'
? deps.discoverLifecycleInstances
: discoverLifecycleInstances;
const isHealthy = typeof deps.isServerHealthReady === 'function'
? deps.isServerHealthReady
: isServerHealthReady;
const [desktopInstance, lifecycleInstances] = await Promise.all([
discoverDesktop(),
discoverLifecycle(options),
]);
if (desktopInstance?.port) {
return desktopInstance.port;
}
const lifecycleDesktop = Array.isArray(lifecycleInstances)
? lifecycleInstances.find((entry) => entry?.runtime === 'desktop' && Number.isFinite(entry.port) && entry.port > 0)
: null;
if (lifecycleDesktop?.port) {
return lifecycleDesktop.port;
}
const ports = uniquePorts(Array.isArray(lifecycleInstances) ? lifecycleInstances : []);
if (ports.length === 1) {
return ports[0];
}
if (ports.length > 1) {
if (ports.includes(DEFAULT_PORT) && await isHealthy(DEFAULT_PORT, 1200)) {
return DEFAULT_PORT;
}
throw new TunnelCliError(
`Multiple OpenChamber instances are running (ports: ${ports.join(', ')}). Choose one with --port <port>.`,
EXIT_CODE.USAGE_ERROR,
);
}
if (await isHealthy(DEFAULT_PORT, 1200)) {
return DEFAULT_PORT;
}
throw new TunnelCliError(
'No running OpenChamber server found. Start one with `openchamber serve`, or pass --port <port>.',
EXIT_CODE.GENERAL_ERROR,
);
}
export { resolveTargetPort };
+251 -1
View File
@@ -63,10 +63,12 @@ function parseArgs(argv = process.argv.slice(2)) {
all: false, all: false,
follow: true, follow: true,
lines: DEFAULT_TAIL_LINES, lines: DEFAULT_TAIL_LINES,
limit: undefined,
provider: undefined, provider: undefined,
mode: undefined, mode: undefined,
profile: undefined, profile: undefined,
name: undefined, name: undefined,
title: undefined,
configPath: undefined, configPath: undefined,
token: undefined, token: undefined,
tokenFile: undefined, tokenFile: undefined,
@@ -88,6 +90,30 @@ function parseArgs(argv = process.argv.slice(2)) {
foreground: false, foreground: false,
lan: false, lan: false,
apiOnly: false, apiOnly: false,
project: undefined,
task: undefined,
session: undefined,
message: undefined,
prompt: undefined,
model: undefined,
daily: undefined,
weekly: undefined,
once: undefined,
time: undefined,
cron: undefined,
timezone: undefined,
agent: undefined,
variant: undefined,
disabled: false,
goal: false,
goalTokenBudget: undefined,
directory: undefined,
role: undefined,
last: false,
wait: false,
timeout: undefined,
lastAssistant: false,
withStatus: false,
}; };
const removedFlagErrors = []; const removedFlagErrors = [];
@@ -194,6 +220,140 @@ function parseArgs(argv = process.argv.slice(2)) {
options.name = typeof value === 'string' ? value : options.name; options.name = typeof value === 'string' ? value : options.name;
break; break;
} }
case 'title': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.title = typeof value === 'string' ? value : options.title;
break;
}
case 'worktree': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.worktree = typeof value === 'string' ? value : options.worktree;
break;
}
case 'branch': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.branch = typeof value === 'string' ? value : options.branch;
break;
}
case 'start-ref':
case 'base': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.startRef = typeof value === 'string' ? value : options.startRef;
break;
}
case 'upstream':
options.setUpstream = true;
break;
case 'no-upstream':
options.setUpstream = false;
break;
case 'project': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.project = typeof value === 'string' ? value : options.project;
break;
}
case 'dir':
case 'directory': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.directory = typeof value === 'string' ? value : options.directory;
break;
}
case 'task': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.task = typeof value === 'string' ? value : options.task;
break;
}
case 'session': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.session = typeof value === 'string' ? value : options.session;
break;
}
case 'message': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.message = typeof value === 'string' ? value : options.message;
break;
}
case 'prompt': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.prompt = typeof value === 'string' ? value : options.prompt;
break;
}
case 'model': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.model = typeof value === 'string' ? value : options.model;
break;
}
case 'daily': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.daily = typeof value === 'string' ? value : options.daily;
break;
}
case 'weekly': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.weekly = typeof value === 'string' ? value : options.weekly;
break;
}
case 'once': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.once = typeof value === 'string' ? value : options.once;
break;
}
case 'time': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.time = typeof value === 'string' ? value : options.time;
break;
}
case 'cron': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.cron = typeof value === 'string' ? value : options.cron;
break;
}
case 'timezone': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.timezone = typeof value === 'string' ? value : options.timezone;
break;
}
case 'agent': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.agent = typeof value === 'string' ? value : options.agent;
break;
}
case 'variant': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.variant = typeof value === 'string' ? value : options.variant;
break;
}
case 'disabled':
options.disabled = true;
break;
case 'goal':
options.goal = true;
break;
case 'goal-token-budget': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.goalTokenBudget = value;
break;
}
case 'config': { case 'config': {
const { value, nextIndex } = consumeValue(i, inlineValue); const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex; i = nextIndex;
@@ -249,6 +409,30 @@ function parseArgs(argv = process.argv.slice(2)) {
case 'all': case 'all':
options.all = true; options.all = true;
break; break;
case 'last':
options.last = true;
break;
case 'last-assistant':
options.lastAssistant = true;
break;
case 'wait':
options.wait = true;
break;
case 'timeout': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.timeout = typeof value === 'string' ? value : options.timeout;
break;
}
case 'with-status':
options.withStatus = true;
break;
case 'role': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.role = typeof value === 'string' ? value : options.role;
break;
}
case 'no-follow': case 'no-follow':
options.follow = false; options.follow = false;
break; break;
@@ -264,6 +448,16 @@ function parseArgs(argv = process.argv.slice(2)) {
} }
break; break;
} }
case 'limit': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
const parsed = parseInt(value ?? '', 10);
if (!Number.isFinite(parsed) || parsed < 1) {
throw new TunnelCliError('Invalid limit value. Provide a positive integer.', EXIT_CODE.USAGE_ERROR);
}
options.limit = parsed;
break;
}
case 'relay': case 'relay':
options.relay = true; options.relay = true;
break; break;
@@ -342,6 +536,9 @@ function parseArgs(argv = process.argv.slice(2)) {
const subcommand = command === 'tunnel' ? (positional[1] || 'help') : null; const subcommand = command === 'tunnel' ? (positional[1] || 'help') : null;
const tunnelAction = command === 'tunnel' ? (positional[2] || null) : null; const tunnelAction = command === 'tunnel' ? (positional[2] || null) : null;
const startupAction = command === 'startup' ? (positional[1] || 'status') : null; const startupAction = command === 'startup' ? (positional[1] || 'status') : null;
const scheduleAction = command === 'schedule' ? (positional[1] || 'help') : null;
const sessionAction = command === 'session' ? (positional[1] || 'help') : null;
const controlAction = command === 'control' ? (positional[1] || 'help') : null;
if (options.lan && typeof options.host !== 'string') { if (options.lan && typeof options.host !== 'string') {
options.host = '0.0.0.0'; options.host = '0.0.0.0';
@@ -356,6 +553,9 @@ function parseArgs(argv = process.argv.slice(2)) {
subcommand, subcommand,
tunnelAction, tunnelAction,
startupAction, startupAction,
scheduleAction,
sessionAction,
controlAction,
options, options,
removedFlagErrors, removedFlagErrors,
helpRequested, helpRequested,
@@ -375,6 +575,11 @@ COMMANDS:
stop Stop running instance(s) stop Stop running instance(s)
restart Stop and start the server restart Stop and start the server
status Show server status status Show server status
schedule Manage scheduled tasks
session Create, inspect, and read OpenChamber sessions
models Show default and favorite models
projects Show configured projects and IDs
control Show OpenChamber control-plane commands
tunnel Tunnel lifecycle commands tunnel Tunnel lifecycle commands
startup Manage launch at system startup startup Manage launch at system startup
logs Tail OpenChamber logs logs Tail OpenChamber logs
@@ -412,12 +617,52 @@ EXAMPLES:
openchamber serve --foreground # Start in foreground (for systemd Type=simple) openchamber serve --foreground # Start in foreground (for systemd Type=simple)
openchamber connect-url --port 3000 --qr openchamber connect-url --port 3000 --qr
openchamber connect-url --server https://openchamber.example.com openchamber connect-url --server https://openchamber.example.com
openchamber control # Show control-plane commands for agents/scripts
openchamber startup enable # Start OpenChamber at user login openchamber startup enable # Start OpenChamber at user login
openchamber tunnel help # Show tunnel lifecycle help openchamber tunnel help # Show tunnel lifecycle help
openchamber logs # Follow logs for latest running instance openchamber logs # Follow logs for latest running instance
`); `);
} }
function showControlHelp() {
console.log(`
OpenChamber Control Commands
USAGE:
openchamber <COMMAND> [OPTIONS]
COMMANDS:
status Show running OpenChamber runtimes
session Create, inspect, and read sessions
models Show default and favorite models
projects Show configured projects and IDs
schedule Manage scheduled tasks
tunnel Inspect tunnel status/readiness
logs Tail logs for CLI-managed runtimes
DETAILED HELP:
openchamber session --help Show session creation, status, and message options
openchamber models --help Show model defaults and favorites help
openchamber projects --help Show project list help
openchamber schedule --help Show scheduled task actions and schedule options
openchamber tunnel help Show tunnel lifecycle/status commands
openchamber status --help Show runtime status options
COMMON OPTIONS:
--json Output machine-readable JSON
-q, --quiet Print minimal output
-p, --port <port> Target a specific OpenChamber runtime
--ui-password <password> Authenticate to a password-protected runtime
EXAMPLES:
openchamber status
openchamber models
openchamber projects
openchamber session --help
openchamber schedule --help
`);
}
function showStartupHelp() { function showStartupHelp() {
console.log(` console.log(`
OpenChamber Startup Commands OpenChamber Startup Commands
@@ -579,7 +824,7 @@ _openchamber_tunnel() {
cur="\${COMP_WORDS[COMP_CWORD]}" cur="\${COMP_WORDS[COMP_CWORD]}"
prev="\${COMP_WORDS[COMP_CWORD-1]}" prev="\${COMP_WORDS[COMP_CWORD-1]}"
commands="serve stop restart status tunnel logs update" commands="serve stop restart status schedule session models projects tunnel logs update"
tunnel_commands="help providers ready doctor status start stop profile completion" tunnel_commands="help providers ready doctor status start stop profile completion"
profile_commands="list show add remove" profile_commands="list show add remove"
common_flags="--port --foreground --no-daemon --json --all --help --version --plain --quiet" common_flags="--port --foreground --no-daemon --json --all --help --version --plain --quiet"
@@ -631,6 +876,10 @@ _openchamber() {
'stop:Stop running instance(s)' 'stop:Stop running instance(s)'
'restart:Stop and start the server' 'restart:Stop and start the server'
'status:Show server status' 'status:Show server status'
'schedule:Manage scheduled tasks'
'session:Create sessions'
'models:Show default and favorite models'
'projects:Show configured projects and IDs'
'tunnel:Tunnel lifecycle commands' 'tunnel:Tunnel lifecycle commands'
'logs:Tail OpenChamber logs' 'logs:Tail OpenChamber logs'
'update:Check for and install updates' 'update:Check for and install updates'
@@ -728,6 +977,7 @@ export {
DEFAULT_PORT, DEFAULT_PORT,
parseArgs, parseArgs,
showHelp, showHelp,
showControlHelp,
showStartupHelp, showStartupHelp,
showConnectUrlHelp, showConnectUrlHelp,
showTunnelHelp, showTunnelHelp,
+42
View File
@@ -0,0 +1,42 @@
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
import { requestJson } from './cli-http.js';
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const DEFAULT_WAIT_TIMEOUT_SECONDS = 600;
const WAIT_HTTP_TIMEOUT_BUFFER_MS = 30_000;
// The control service blocks server-side while wait is set, so the client
// HTTP timeout must outlive the requested wait window instead of the short
// default used for instant control calls.
export const resolveControlTimeoutMs = (input, options) => {
if (Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0) return options.timeoutMs;
if (input?.wait !== true) return undefined;
const waitSeconds = Number(input?.timeout) > 0 ? Number(input.timeout) : DEFAULT_WAIT_TIMEOUT_SECONDS;
return (waitSeconds * 1000) + WAIT_HTTP_TIMEOUT_BUFFER_MS;
};
export const requestControlAction = async (port, action, input, options = {}) => {
const timeoutMs = resolveControlTimeoutMs(input, options);
const { response, body } = await requestJson(port, '/api/openchamber/control', {
...options,
...(timeoutMs ? { timeoutMs } : {}),
method: 'POST',
body: JSON.stringify({ action, input }),
});
if (response?.ok) return body;
const isPartial = body?.partial === true;
const partialSessionId = isPartial ? asNonEmptyString(body?.sessionId) : null;
const partialDirectory = isPartial ? asNonEmptyString(body?.directory) : null;
const partialSubject = body?.partialAction === 'goal-configured' ? 'Goal on session' : 'Forked session';
const partial = partialSessionId
? ` ${partialSubject} ${partialSessionId} remains available${partialDirectory ? ` in ${partialDirectory}` : ''}.`
: '';
const message = `${asNonEmptyString(body?.error) || `Failed to execute ${action}`}${partial}`;
const status = Number(response?.status);
throw new TunnelCliError(message, status === 400 || status === 404 ? EXIT_CODE.USAGE_ERROR : EXIT_CODE.GENERAL_ERROR);
};
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { resolveControlTimeoutMs } from './cli-control.js';
describe('resolveControlTimeoutMs', () => {
it('keeps the short default HTTP timeout for instant control calls', () => {
expect(resolveControlTimeoutMs({}, {})).toBeUndefined();
expect(resolveControlTimeoutMs({ wait: false, timeout: 30 }, {})).toBeUndefined();
});
it('outlives the default server wait window when wait is set', () => {
expect(resolveControlTimeoutMs({ wait: true }, {})).toBe(630_000);
});
it('derives the HTTP timeout from an explicit wait timeout in seconds', () => {
expect(resolveControlTimeoutMs({ wait: true, timeout: 30 }, {})).toBe(60_000);
});
it('never shrinks an explicitly requested HTTP timeout', () => {
expect(resolveControlTimeoutMs({ wait: true, timeout: 30 }, { timeoutMs: 5000 })).toBe(5000);
});
});
+17
View File
@@ -0,0 +1,17 @@
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
export const parseGoalTokenBudget = (options) => {
if (options.goalTokenBudget === undefined) return undefined;
if (options.goal !== true) {
throw new TunnelCliError('--goal-token-budget requires --goal.', EXIT_CODE.USAGE_ERROR);
}
const raw = String(options.goalTokenBudget).trim();
if (!/^\d+$/.test(raw)) {
throw new TunnelCliError('--goal-token-budget must be an integer from 1000 to 100000000.', EXIT_CODE.USAGE_ERROR);
}
const budget = Number(raw);
if (!Number.isSafeInteger(budget) || budget < 1000 || budget > 100000000) {
throw new TunnelCliError('--goal-token-budget must be an integer from 1000 to 100000000.', EXIT_CODE.USAGE_ERROR);
}
return budget;
};
+44 -8
View File
@@ -1,24 +1,44 @@
import { buildLocalUrl } from './cli-network.js'; import { buildLocalUrl } from './cli-network.js';
import { readDesktopLocalClientTokenFromSettings, readDesktopLocalPortFromSettings } from './cli-paths.js';
import { getInstanceFilePath, readInstanceOptions } from './cli-process.js'; import { getInstanceFilePath, readInstanceOptions } from './cli-process.js';
const UI_SESSION_COOKIE_NAME = 'oc_ui_session'; const UI_SESSION_COOKIE_NAME = 'oc_ui_session';
function extractUiSessionCookie(response) { function extractUiSessionCookie(response) {
const setCookie = response?.headers?.get?.('set-cookie'); const values = [];
if (typeof setCookie !== 'string' || setCookie.length === 0) { const direct = response?.headers?.get?.('set-cookie');
return null; if (typeof direct === 'string' && direct.length > 0) {
values.push(direct);
} }
const match = setCookie.match(new RegExp(`(?:^|,\\s*)(${UI_SESSION_COOKIE_NAME}=[^;]+)`)); const getSetCookie = response?.headers?.getSetCookie;
return match?.[1] || null; if (typeof getSetCookie === 'function') {
const setCookies = getSetCookie.call(response.headers);
if (Array.isArray(setCookies)) {
values.push(...setCookies.filter((value) => typeof value === 'string' && value.length > 0));
}
}
const raw = response?.headers?.raw?.();
if (Array.isArray(raw?.['set-cookie'])) {
values.push(...raw['set-cookie'].filter((value) => typeof value === 'string' && value.length > 0));
}
for (const setCookie of values) {
const match = setCookie.match(new RegExp(`(?:^|,\\s*)(${UI_SESSION_COOKIE_NAME}=[^;]+)`));
if (match?.[1]) return match[1];
}
return null;
} }
async function resolveUiPasswordForPort(port, options = {}) { async function resolveUiPasswordForPort(port, options = {}) {
if (typeof options.uiPassword === 'string' && options.uiPassword.trim().length > 0) { if (options.explicitUiPassword && typeof options.uiPassword === 'string' && options.uiPassword.trim().length > 0) {
return options.uiPassword; return options.uiPassword;
} }
const instanceOptions = readInstanceOptions(await getInstanceFilePath(port)); const instanceOptions = readInstanceOptions(await getInstanceFilePath(port));
return typeof instanceOptions?.uiPassword === 'string' && instanceOptions.uiPassword.trim().length > 0 if (typeof instanceOptions?.uiPassword === 'string' && instanceOptions.uiPassword.trim().length > 0) {
? instanceOptions.uiPassword return instanceOptions.uiPassword;
}
return typeof options.uiPassword === 'string' && options.uiPassword.trim().length > 0
? options.uiPassword
: null; : null;
} }
@@ -49,6 +69,18 @@ async function createUiSessionCookie(port, password, timeoutMs) {
} }
} }
function getDesktopLocalAuthHeader(port, requestHeaders) {
if (requestHeaders.Authorization || requestHeaders.authorization) {
return null;
}
const desktopPort = readDesktopLocalPortFromSettings();
if (desktopPort !== port) {
return null;
}
const token = readDesktopLocalClientTokenFromSettings();
return token ? `Bearer ${token}` : null;
}
async function requestServerShutdown(port, hostOverride) { async function requestServerShutdown(port, hostOverride) {
if (!Number.isFinite(port) || port <= 0) return false; if (!Number.isFinite(port) || port <= 0) return false;
const controller = new AbortController(); const controller = new AbortController();
@@ -83,6 +115,10 @@ async function requestJson(port, endpoint, options = {}) {
...(fetchOptions.body ? { 'Content-Type': 'application/json' } : {}), ...(fetchOptions.body ? { 'Content-Type': 'application/json' } : {}),
...(fetchOptions.headers || {}), ...(fetchOptions.headers || {}),
}; };
const desktopAuth = getDesktopLocalAuthHeader(port, requestHeaders);
if (desktopAuth) {
requestHeaders.Authorization = desktopAuth;
}
const response = await fetch(requestUrl, { const response = await fetch(requestUrl, {
...fetchOptions, ...fetchOptions,
headers: requestHeaders, headers: requestHeaders,
+12
View File
@@ -35,6 +35,17 @@ function readDesktopLocalPortFromSettings() {
} }
} }
function readDesktopLocalClientTokenFromSettings() {
try {
const raw = fs.readFileSync(getSettingsFilePath(), 'utf8');
const parsed = JSON.parse(raw);
const value = parsed?.desktopLocalClientToken;
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : '';
} catch {
return '';
}
}
function ensureLogsDir() { function ensureLogsDir() {
fs.mkdirSync(getLogsDir(), { recursive: true }); fs.mkdirSync(getLogsDir(), { recursive: true });
} }
@@ -103,6 +114,7 @@ function getRunDir() {
export { export {
getDataDir, getDataDir,
readDesktopLocalPortFromSettings, readDesktopLocalPortFromSettings,
readDesktopLocalClientTokenFromSettings,
ensureLogsDir, ensureLogsDir,
getLogFilePath, getLogFilePath,
getTunnelProfilesFilePath, getTunnelProfilesFilePath,
+63
View File
@@ -0,0 +1,63 @@
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
import { resolveTargetPort } from './cli-api-target.js';
import { requestControlAction } from './cli-control.js';
import { isJsonMode, printJson } from '../cli-output.js';
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const formatModelRef = (entry) => {
const providerID = asNonEmptyString(entry?.providerID) || asNonEmptyString(entry?.providerId);
const modelID = asNonEmptyString(entry?.modelID) || asNonEmptyString(entry?.modelId) || asNonEmptyString(entry?.id);
return providerID && modelID ? `${providerID}/${modelID}` : null;
};
const formatDefaultLine = ({ defaultModel, defaultAgent, defaultVariant }) => {
const model = asNonEmptyString(defaultModel) || 'none';
const agent = asNonEmptyString(defaultAgent) || 'none';
const variant = asNonEmptyString(defaultVariant);
return `Default: \`${model}${variant ? ` (${variant})` : ''}\` / \`${agent}\``;
};
const formatModelsOutput = (settings = {}) => {
const favorites = Array.isArray(settings.favoriteModels)
? settings.favoriteModels.map(formatModelRef).filter(Boolean)
: [];
const recent = Array.isArray(settings.recentModels)
? settings.recentModels.map(formatModelRef).filter(Boolean)
: [];
const lines = [
formatDefaultLine(settings),
'',
'Favorites:',
...(favorites.length > 0 ? favorites.map((model) => `- \`${model}\``) : ['- none']),
'',
'Recent:',
...(recent.length > 0 ? recent.map((model) => `- \`${model}\``) : ['- none']),
];
return `${lines.join('\n')}\n`;
};
async function modelsCommand(options = {}, action = 'show') {
if (action === 'help') {
process.stdout.write(`OpenChamber Models Commands\n\nUSAGE:\n openchamber models [OPTIONS]\n\nOUTPUT OPTIONS:\n -p, --port <port> OpenChamber server port\n --json Output machine-readable JSON\n`);
return;
}
if (action !== 'show') {
throw new TunnelCliError(`Unknown models command '${action}'.`, EXIT_CODE.USAGE_ERROR);
}
const port = await resolveTargetPort(options);
const result = await requestControlAction(port, 'models.list', {}, options);
if (isJsonMode(options)) {
printJson(result);
return;
}
process.stdout.write(formatModelsOutput(result));
}
export { modelsCommand, formatModelsOutput, formatDefaultLine, formatModelRef };
+28
View File
@@ -0,0 +1,28 @@
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
import { resolveTargetPort } from './cli-api-target.js';
import { requestControlAction } from './cli-control.js';
import { isJsonMode, printJson } from '../cli-output.js';
const formatProjectLine = (project) => `- \`${project.label}\` — \`${project.id}\` — \`${project.path}\``;
async function projectsCommand(options = {}, action = 'list') {
if (action === 'help') {
process.stdout.write(`OpenChamber Projects Commands\n\nUSAGE:\n openchamber projects [OPTIONS]\n\nOUTPUT OPTIONS:\n -p, --port <port> OpenChamber server port\n --json Output machine-readable JSON\n`);
return;
}
if (action !== 'list') {
throw new TunnelCliError(`Unknown projects command '${action}'.`, EXIT_CODE.USAGE_ERROR);
}
const port = await resolveTargetPort(options);
const { projects = [] } = await requestControlAction(port, 'projects.list', {}, options);
if (isJsonMode(options)) {
printJson({ projects });
return;
}
process.stdout.write(projects.length > 0
? `${projects.map(formatProjectLine).join('\n')}\n`
: 'No projects found.\n');
}
export { projectsCommand, formatProjectLine };
+195
View File
@@ -0,0 +1,195 @@
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
import { resolveTargetPort } from './cli-api-target.js';
import { parseGoalTokenBudget } from './cli-goal.js';
import { requestControlAction } from './cli-control.js';
import {
intro as clackIntro,
outro as clackOutro,
isJsonMode,
isQuietMode,
printJson,
logStatus,
} from '../cli-output.js';
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const assertRequired = (value, flagName) => {
const normalized = asNonEmptyString(value);
if (!normalized) {
throw new TunnelCliError(`Missing required ${flagName}.`, EXIT_CODE.USAGE_ERROR);
}
return normalized;
};
const formatGoal = (execution) => {
if (execution?.goalEnabled !== true) return 'goal:no';
return Number.isFinite(execution.goalTokenBudget)
? `goal:yes budget:${execution.goalTokenBudget}`
: 'goal:yes';
};
const formatSchedule = (schedule) => {
if (!schedule || typeof schedule !== 'object') return 'unknown';
if (schedule.kind === 'daily') return `daily ${Array.isArray(schedule.times) ? schedule.times.join(',') : ''}`.trim();
if (schedule.kind === 'weekly') return `weekly days:${Array.isArray(schedule.weekdays) ? schedule.weekdays.join(',') : ''} time:${Array.isArray(schedule.times) ? schedule.times.join(',') : ''}`;
if (schedule.kind === 'once') return `once ${schedule.date || ''} ${schedule.time || ''}`.trim();
if (schedule.kind === 'cron') return `cron ${schedule.cron || ''}`.trim();
return schedule.kind || 'unknown';
};
const outputTasks = (options, tasks) => {
const normalizedTasks = Array.isArray(tasks) ? tasks : [];
if (isJsonMode(options)) {
printJson({ tasks: normalizedTasks });
return;
}
if (isQuietMode(options)) {
for (const task of normalizedTasks) {
process.stdout.write(`${task.id} enabled:${task.enabled === false ? 'no' : 'yes'} ${formatGoal(task.execution)} status:${task.state?.lastStatus || 'idle'} ${formatSchedule(task.schedule)} ${task.name || ''}\n`);
}
return;
}
clackIntro('Scheduled Tasks');
if (normalizedTasks.length === 0) {
logStatus('info', 'No scheduled tasks found');
clackOutro('0 tasks');
return;
}
for (const task of normalizedTasks) {
const status = task.enabled === false ? 'warning' : 'success';
const detail = `id: ${task.id}; ${formatGoal(task.execution)}; status: ${task.state?.lastStatus || 'idle'}; ${formatSchedule(task.schedule)}`;
logStatus(status, task.name || task.id, detail);
}
clackOutro(`${normalizedTasks.length} task(s)`);
};
async function scheduleCommand(options = {}, action = 'help') {
if (action === 'help') {
process.stdout.write(`OpenChamber Schedule Commands\n\nUSAGE:\n openchamber schedule status [OPTIONS]\n openchamber schedule list (--project <projectId> | --dir <path>) [OPTIONS]\n openchamber schedule create (--project <projectId> | --dir <path>) --name <name> --prompt <prompt> --model <provider/model> (--daily <HH:mm> | --weekly <0,1,2> --time <HH:mm> | --once <YYYY-MM-DD> --time <HH:mm> | --cron <expr>) [OPTIONS]\n openchamber schedule run (--project <projectId> | --dir <path>) --task <taskId> [OPTIONS]\n openchamber schedule delete (--project <projectId> | --dir <path>) --task <taskId> [OPTIONS]\n openchamber schedule enable (--project <projectId> | --dir <path>) --task <taskId> [OPTIONS]\n openchamber schedule disable (--project <projectId> | --dir <path>) --task <taskId> [OPTIONS]\n\nOPTIONS:\n --project <projectId> Project id from openchamber projects\n --dir <path> Resolve project by directory\n -p, --port <port> OpenChamber server port\n --timezone <zone> IANA timezone for created tasks\n --agent <id> Agent to use when running task\n --variant <id> Model variant to use when running task\n --goal Continue the scheduled session toward a goal\n --goal-token-budget <n> Goal token budget (1000-100000000; requires --goal)\n --disabled Create task disabled\n --json Output machine-readable JSON\n -q, --quiet Print concise output\n`);
return;
}
const port = await resolveTargetPort(options);
const target = {
...(asNonEmptyString(options.project) ? { projectId: options.project.trim() } : {}),
...(asNonEmptyString(options.directory) ? { directory: options.directory.trim() } : {}),
};
if (action === 'status') {
const body = await requestControlAction(port, 'schedule.status', {}, options);
if (isJsonMode(options)) {
printJson(body || {});
return;
}
if (isQuietMode(options)) {
process.stdout.write(`enabled:${body?.enabledScheduledTasksCount ?? 0} running:${body?.runningScheduledTasksCount ?? 0}\n`);
return;
}
clackIntro('Scheduled Task Status');
logStatus(body?.hasEnabledScheduledTasks ? 'success' : 'info', `enabled: ${body?.enabledScheduledTasksCount ?? 0}`);
logStatus(body?.hasRunningScheduledTasks ? 'success' : 'info', `running: ${body?.runningScheduledTasksCount ?? 0}`);
clackOutro('status loaded');
return;
}
if (action === 'list') {
const body = await requestControlAction(port, 'schedule.list', target, options);
outputTasks(options, body?.tasks);
return;
}
if (action === 'create') {
const goalTokenBudget = parseGoalTokenBudget(options);
const input = {
...target,
name: options.name,
prompt: options.prompt,
model: options.model,
daily: options.daily,
weekly: options.weekly,
once: options.once,
time: options.time,
cron: options.cron,
timezone: options.timezone,
agent: options.agent,
variant: options.variant,
goal: options.goal === true,
...(goalTokenBudget !== undefined ? { goalTokenBudget } : {}),
disabled: options.disabled === true,
};
const body = await requestControlAction(port, 'schedule.create', input, options);
if (isJsonMode(options)) {
printJson({ task: body?.task, created: body?.created === true });
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${body?.task?.id || ''}\n`);
return;
}
clackIntro('Scheduled Task Created');
logStatus('success', body?.task?.name || options.name, `id: ${body?.task?.id || 'unknown'}; ${formatGoal(body?.task?.execution)}; ${formatSchedule(body?.task?.schedule)}`);
clackOutro('created');
return;
}
if (action === 'run') {
const taskID = assertRequired(options.task, '--task');
const body = await requestControlAction(port, 'schedule.run', { ...target, taskId: taskID }, options);
if (isJsonMode(options)) {
printJson({ task: body?.task, sessionId: body?.sessionId });
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${body?.sessionId || ''}\n`);
return;
}
clackIntro('Scheduled Task Run');
logStatus('success', body?.task?.name || taskID, `session: ${body?.sessionId || 'unknown'}`);
clackOutro('started');
return;
}
if (action === 'delete') {
const taskID = assertRequired(options.task, '--task');
const body = await requestControlAction(port, 'schedule.delete', { ...target, taskId: taskID }, options);
if (isJsonMode(options)) {
printJson({ deleted: true, tasks: body?.tasks || [] });
return;
}
if (isQuietMode(options)) {
process.stdout.write(`deleted ${taskID}\n`);
return;
}
clackIntro('Scheduled Task Deleted');
logStatus('success', `deleted ${taskID}`);
clackOutro('deleted');
return;
}
if (action === 'enable' || action === 'disable') {
const taskID = assertRequired(options.task, '--task');
const enabled = action === 'enable';
const { task } = await requestControlAction(port, 'schedule.toggle', { ...target, taskId: taskID, disabled: !enabled }, options);
if (isJsonMode(options)) {
printJson({ task, enabled });
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${taskID} enabled:${enabled ? 'yes' : 'no'}\n`);
return;
}
clackIntro(enabled ? 'Scheduled Task Enabled' : 'Scheduled Task Disabled');
logStatus('success', task?.name || taskID, `enabled: ${enabled ? 'yes' : 'no'}`);
clackOutro(enabled ? 'enabled' : 'disabled');
return;
}
throw new TunnelCliError(`Unknown schedule command '${action}'.`, EXIT_CODE.USAGE_ERROR);
}
export { scheduleCommand, formatGoal };
+340
View File
@@ -0,0 +1,340 @@
import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
import { resolveTargetPort } from './cli-api-target.js';
import { parseGoalTokenBudget } from './cli-goal.js';
import { requestControlAction } from './cli-control.js';
import {
intro as clackIntro,
outro as clackOutro,
isJsonMode,
isQuietMode,
printJson,
logStatus,
} from '../cli-output.js';
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const validateModel = (model) => {
const normalized = asNonEmptyString(model);
if (!normalized) return null;
const slashIndex = normalized.indexOf('/');
if (slashIndex <= 0 || slashIndex === normalized.length - 1) {
throw new TunnelCliError('--model must be in provider/model format.', EXIT_CODE.USAGE_ERROR);
}
return normalized;
};
const normalizeLimit = (value, fallback = 10) => {
if (value === undefined || value === null) return fallback;
const parsed = Number.parseInt(String(value), 10);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new TunnelCliError('Invalid limit value. Provide a positive integer.', EXIT_CODE.USAGE_ERROR);
}
return parsed;
};
const assertSessionTarget = (options = {}) => {
const sessionId = asNonEmptyString(options.session);
const directory = asNonEmptyString(options.directory);
if (!sessionId) {
throw new TunnelCliError('Missing required --session.', EXIT_CODE.USAGE_ERROR);
}
if (!directory) {
throw new TunnelCliError('Missing required --dir.', EXIT_CODE.USAGE_ERROR);
}
return { sessionId, directory };
};
const normalizeMessageRole = (value) => {
const role = asNonEmptyString(value) || 'all';
if (!['all', 'user', 'assistant'].includes(role)) {
throw new TunnelCliError('--role must be one of: all, user, assistant.', EXIT_CODE.USAGE_ERROR);
}
return role;
};
const formatTextMessage = (message) => {
const label = message.role === 'user' ? 'User' : 'Assistant';
const timestamp = message.createdAt ? new Date(message.createdAt).toISOString() : '';
const details = [timestamp, message.model].filter(Boolean).join(' ');
return `**${label}**${details ? `\n\n*${details}*` : ''}\n\n${message.text}`;
};
const formatSessionModel = (session) => {
const model = session?.model;
const providerID = asNonEmptyString(model?.providerID) || asNonEmptyString(model?.providerId);
const modelID = asNonEmptyString(model?.id) || asNonEmptyString(model?.modelID) || asNonEmptyString(model?.modelId);
return providerID && modelID ? `${providerID}/${modelID}` : null;
};
const formatSessionLine = (session) => {
const title = asNonEmptyString(session?.title) || asNonEmptyString(session?.slug) || asNonEmptyString(session?.id) || 'untitled';
const model = formatSessionModel(session) || 'unknown-model';
const agent = asNonEmptyString(session?.agent) || 'unknown-agent';
const variant = asNonEmptyString(session?.model?.variant);
const directory = asNonEmptyString(session?.directory) || 'unknown-directory';
const selections = [`\`${model}\``, `\`${agent}\``];
if (variant && variant !== 'default') selections.push(`\`${variant}\``);
const status = asNonEmptyString(session?.status?.type);
return `- \`${title}\` — ${selections.join(', ')}${status ? ` — status:${status}` : ''} — \`${directory}\``;
};
const buildSessionCreatePayload = (options = {}) => {
const directory = asNonEmptyString(options.directory);
const projectId = asNonEmptyString(options.project);
if (!directory && !projectId) {
throw new TunnelCliError('Missing required --dir or --project.', EXIT_CODE.USAGE_ERROR);
}
if (directory && projectId) {
throw new TunnelCliError('Provide only one of --dir or --project.', EXIT_CODE.USAGE_ERROR);
}
const prompt = asNonEmptyString(options.prompt);
const model = validateModel(options.model);
const goalEnabled = options.goal === true;
const goalTokenBudget = parseGoalTokenBudget(options);
if (goalEnabled && !prompt) {
throw new TunnelCliError('--goal requires --prompt.', EXIT_CODE.USAGE_ERROR);
}
const title = asNonEmptyString(options.title) || asNonEmptyString(options.name);
const agent = asNonEmptyString(options.agent);
const variant = asNonEmptyString(options.variant);
const worktree = asNonEmptyString(options.worktree);
const branch = asNonEmptyString(options.branch);
const startRef = asNonEmptyString(options.startRef);
return {
...(directory ? { directory } : {}),
...(projectId ? { projectId } : {}),
...(title ? { title } : {}),
...(worktree ? { worktree: { name: worktree, ...(branch ? { branchName: branch } : {}), ...(startRef ? { startRef } : {}) } } : {}),
...(prompt ? { prompt } : {}),
...(model ? { model } : {}),
...(agent ? { agent } : {}),
...(variant ? { variant } : {}),
...(goalEnabled ? { goal: true } : {}),
...(goalTokenBudget !== undefined ? { goalTokenBudget } : {}),
...(typeof options.setUpstream === 'boolean' ? { setUpstream: options.setUpstream } : {}),
};
};
const buildSessionPromptPayload = (options = {}, action) => {
const { directory } = assertSessionTarget(options);
const prompt = asNonEmptyString(options.prompt);
if (!prompt) throw new TunnelCliError('Missing required --prompt.', EXIT_CODE.USAGE_ERROR);
const model = validateModel(options.model);
const agent = asNonEmptyString(options.agent);
const variant = asNonEmptyString(options.variant);
const messageId = asNonEmptyString(options.message);
if (messageId && action !== 'fork') {
throw new TunnelCliError('--message is only valid for session fork.', EXIT_CODE.USAGE_ERROR);
}
const goalEnabled = options.goal === true;
const goalTokenBudget = parseGoalTokenBudget(options);
return {
directory,
prompt,
...(messageId ? { messageId } : {}),
...(model ? { model } : {}),
...(agent ? { agent } : {}),
...(variant ? { variant } : {}),
...(goalEnabled ? { goal: true } : {}),
...(goalTokenBudget !== undefined ? { goalTokenBudget } : {}),
};
};
const validateActionWaitOptions = (options, action) => {
if (options.timeout !== undefined && !options.wait) {
throw new TunnelCliError('--timeout requires --wait.', EXIT_CODE.USAGE_ERROR);
}
if (options.lastAssistant && !options.wait) {
throw new TunnelCliError(`--last-assistant requires --wait for session ${action}.`, EXIT_CODE.USAGE_ERROR);
}
};
async function sessionCommand(options = {}, action = 'help') {
if (action === 'help') {
process.stdout.write(`OpenChamber Session Commands\n\nUSAGE:\n openchamber session list [--dir <path>] [--limit <count>] [--with-status] [OPTIONS]\n openchamber session create --dir <path> [--title <title>] [--wait] [OPTIONS]\n openchamber session create --project <projectId> [--title <title>] [--wait] [OPTIONS]\n openchamber session send --session <id> --dir <path> --prompt <text> [--wait] [OPTIONS]\n openchamber session fork --session <id> --dir <path> --prompt <text> [--message <id>] [--wait] [OPTIONS]\n openchamber session status --session <id> --dir <path> [OPTIONS]\n openchamber session messages --session <id> --dir <path> [--wait] [OPTIONS]\n\nLIST OPTIONS:\n --dir <path> Filter sessions by directory\n --limit <count> Maximum sessions to show (default: 10)\n --all Include archived sessions\n --with-status Include authoritative idle/busy/retry status\n\nACTION OPTIONS:\n --session <id> Source or target session id\n --dir <path> Authoritative session directory\n --prompt <text> Prompt to send to the session\n --message <id> Fork from this message (fork only; default: latest)\n --model <provider/model> Model for the prompt (defaults to configured selection)\n --agent <id> Agent for the prompt (defaults to configured selection)\n --variant <id> Model variant for the prompt\n --goal Run the prompt as a new goal\n --goal-token-budget <n> Goal token budget (1000-100000000; requires --goal)\n --wait Wait for the dispatched activity to become idle\n --last-assistant Include the last assistant text after waiting\n --timeout <seconds> Wait timeout in seconds (default: 600, max: 86400)\n\nCREATE OPTIONS:\n --worktree <name> Create a git worktree before creating the session\n --branch <name> Branch name for --worktree\n --start-ref, --base <ref> Start ref for --worktree\n --upstream Set upstream for the worktree branch\n --no-upstream Do not set upstream for the worktree branch\n --name <title> Alias for --title\n\nSTATUS/MESSAGES OPTIONS:\n --last Return only the latest text-bearing message\n --last-assistant Shorthand for --last --role assistant\n --limit <count> Maximum text messages to return (default: 10)\n --all Return all text-bearing messages\n --role <role> Filter messages: all, user, assistant\n\nOUTPUT OPTIONS:\n -p, --port <port> OpenChamber server port\n --json Output machine-readable JSON\n -q, --quiet Print compact output\n`);
return;
}
if (action === 'list') {
const limit = normalizeLimit(options.limit);
const port = await resolveTargetPort(options);
const body = await requestControlAction(port, 'session.list', {
...(asNonEmptyString(options.directory) ? { directory: options.directory.trim() } : {}),
limit,
all: options.all === true,
withStatus: options.withStatus === true,
}, options);
const sessions = Array.isArray(body?.sessions) ? body.sessions : [];
if (isJsonMode(options)) {
printJson(body);
return;
}
process.stdout.write(sessions.length > 0
? `${sessions.map(formatSessionLine).join('\n')}\n`
: 'No sessions found.\n');
return;
}
if (action === 'status') {
const { sessionId, directory } = assertSessionTarget(options);
const port = await resolveTargetPort(options);
const result = await requestControlAction(port, 'session.status', { sessionId, directory }, options);
const status = result.sessionStatus;
if (isJsonMode(options)) {
printJson(result);
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${status.type}\n`);
return;
}
process.stdout.write(`${sessionId} status:${status.type} directory:${directory}\n`);
return;
}
if (action === 'messages') {
const { sessionId, directory } = assertSessionTarget(options);
if (options.timeout !== undefined && !options.wait) {
throw new TunnelCliError('--timeout requires --wait.', EXIT_CODE.USAGE_ERROR);
}
if (options.lastAssistant && options.role && options.role !== 'assistant') {
throw new TunnelCliError('--last-assistant cannot be combined with a non-assistant --role.', EXIT_CODE.USAGE_ERROR);
}
const role = options.lastAssistant ? 'assistant' : normalizeMessageRole(options.role);
const last = options.last || options.lastAssistant;
if (options.all && (last || options.limit !== undefined)) {
throw new TunnelCliError('--all cannot be combined with --last or --limit.', EXIT_CODE.USAGE_ERROR);
}
if (last && options.limit !== undefined) {
throw new TunnelCliError('--last cannot be combined with --limit.', EXIT_CODE.USAGE_ERROR);
}
const limit = options.all ? undefined : (last ? undefined : normalizeLimit(options.limit));
const port = await resolveTargetPort(options);
const result = await requestControlAction(port, 'session.messages', {
sessionId,
directory,
role,
all: options.all === true,
last,
...(limit !== undefined ? { limit } : {}),
wait: options.wait === true,
...(options.timeout !== undefined ? { timeout: Number(options.timeout) } : {}),
lastAssistant: options.lastAssistant === true,
}, options);
const messages = Array.isArray(result?.messages) ? result.messages : [];
if (isJsonMode(options)) {
printJson(result);
return;
}
if (messages.length === 0) {
process.stdout.write('No text messages found.\n');
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${messages.map((message) => message.text).join('\n\n')}\n`);
return;
}
process.stdout.write(`${messages.map(formatTextMessage).join('\n\n---\n\n')}\n`);
return;
}
if (action === 'send' || action === 'fork') {
const { sessionId } = assertSessionTarget(options);
const payload = buildSessionPromptPayload(options, action);
validateActionWaitOptions(options, action);
const port = await resolveTargetPort(options);
const result = await requestControlAction(port, `session.${action}`, {
...payload,
sessionId,
wait: options.wait === true,
...(options.timeout !== undefined ? { timeout: Number(options.timeout) } : {}),
lastAssistant: options.lastAssistant === true,
}, options);
const { sessionStatus, lastAssistantMessage } = result;
if (isJsonMode(options)) {
printJson(result);
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${result?.sessionId || ''}\n`);
if (lastAssistantMessage?.text) process.stdout.write(`${lastAssistantMessage.text}\n`);
return;
}
clackIntro(action === 'fork' ? 'Session Forked' : 'Session Prompt Sent');
logStatus('success', result?.sessionId || `${action} completed`, `directory: ${result?.directory || 'unknown'}`);
if (result?.promptDispatched) {
logStatus('info', result.dispatchedAsCommand ? 'command dispatched' : 'prompt dispatched');
}
if (result?.goalEnabled) {
logStatus('info', 'goal mode active', result.goalTokenBudget ? `budget: ${result.goalTokenBudget}` : undefined);
}
if (sessionStatus) logStatus('info', `session status: ${sessionStatus.type}`);
clackOutro(action === 'fork' ? 'forked' : 'sent');
if (lastAssistantMessage) process.stdout.write(`\n${formatTextMessage(lastAssistantMessage)}\n`);
return;
}
if (action !== 'create') {
throw new TunnelCliError(`Unknown session command '${action}'.`, EXIT_CODE.USAGE_ERROR);
}
const payload = buildSessionCreatePayload(options);
validateActionWaitOptions(options, 'create');
const port = await resolveTargetPort(options);
const controlPayload = { ...payload };
if (payload.worktree) {
controlPayload.worktree = payload.worktree.name;
controlPayload.branch = payload.worktree.branchName;
controlPayload.startRef = payload.worktree.startRef;
}
const result = await requestControlAction(port, 'session.create', {
...controlPayload,
wait: options.wait === true,
...(options.timeout !== undefined ? { timeout: Number(options.timeout) } : {}),
lastAssistant: options.lastAssistant === true,
}, options);
const { sessionStatus, lastAssistantMessage } = result;
if (isJsonMode(options)) {
printJson(result);
return;
}
if (isQuietMode(options)) {
process.stdout.write(`${result?.sessionId || ''}\n`);
if (lastAssistantMessage?.text) process.stdout.write(`${lastAssistantMessage.text}\n`);
return;
}
clackIntro('Session Created');
logStatus('success', result?.sessionId || 'session created', `directory: ${result?.directory || 'unknown'}`);
if (result?.worktree?.path) {
logStatus('info', `worktree: ${result.worktree.branch || result.worktree.name || 'created'}`, result.worktree.path);
}
if (result?.promptDispatched) {
logStatus('info', result.dispatchedAsCommand ? 'initial command dispatched' : 'initial prompt dispatched');
}
if (result?.goalEnabled) {
logStatus('info', 'goal mode active', result.goalTokenBudget ? `budget: ${result.goalTokenBudget}` : undefined);
}
if (sessionStatus) {
logStatus('info', `session status: ${sessionStatus.type}`);
}
clackOutro('created');
if (lastAssistantMessage) {
process.stdout.write(`\n${formatTextMessage(lastAssistantMessage)}\n`);
}
}
export {
sessionCommand,
buildSessionCreatePayload,
buildSessionPromptPayload,
formatSessionLine,
};
+73
View File
@@ -95,6 +95,10 @@ import { createPreviewProxyRuntime } from './lib/preview/proxy-runtime.js';
import { attachRealtimeProxy } from './lib/realtime-proxy.js'; import { attachRealtimeProxy } from './lib/realtime-proxy.js';
import { createRelayService } from './lib/relay/service.js'; import { createRelayService } from './lib/relay/service.js';
import { createRelayHostLock } from './lib/relay/host-lock.js'; import { createRelayHostLock } from './lib/relay/host-lock.js';
import { createAgentToolRuntime } from './lib/agent-tool/runtime.js';
import { createOpenChamberSessionService } from './lib/openchamber-sessions/routes.js';
import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
import { createOpenChamberControlService } from './lib/openchamber-control/service.js';
import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware'; import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
import webPush from 'web-push'; import webPush from 'web-push';
@@ -264,6 +268,7 @@ const themeRuntime = createThemeRuntime({
const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromDisk(...args); const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromDisk(...args);
let notificationTemplateRuntime = null; let notificationTemplateRuntime = null;
let agentToolRuntime = null;
const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args); const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args); const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
@@ -952,6 +957,7 @@ const bootstrapRuntime = createBootstrapRuntime({
registerTtsRoutes, registerTtsRoutes,
registerNotificationRoutes, registerNotificationRoutes,
registerOpenChamberRoutes, registerOpenChamberRoutes,
registerAgentToolRoutes: (app, options) => options.agentToolRuntime.registerRoutes(app, options.express),
express, express,
}); });
const tunnelWiringRuntime = createTunnelWiringRuntime({ const tunnelWiringRuntime = createTunnelWiringRuntime({
@@ -1048,6 +1054,11 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
buildManagedOpenCodePath, buildManagedOpenCodePath,
getManagedOpenCodeShellEnvSnapshot: getLoginShellEnvSnapshot, getManagedOpenCodeShellEnvSnapshot: getLoginShellEnvSnapshot,
getActiveSessionCount, getActiveSessionCount,
getManagedOpenCodeEnv: async () => {
const settings = await readSettingsFromDiskMigrated().catch(() => null);
if (settings?.agentControlToolEnabled === false) return {};
return agentToolRuntime?.prepareManagedOpenCodeEnv() || {};
},
}); });
const restartOpenCode = (...args) => openCodeLifecycleRuntime.restartOpenCode(...args); const restartOpenCode = (...args) => openCodeLifecycleRuntime.restartOpenCode(...args);
@@ -1086,6 +1097,50 @@ const scheduledTasksRuntime = createScheduledTasksRuntime({
}, },
logger: console, logger: console,
}); });
const emitSessionCreatedEvent = (event) => {
for (const client of uiOpenChamberEventClients) {
try {
writeSseEvent(client, {
type: 'openchamber:session-created',
properties: {
sessionId: event.sessionID,
directory: event.directory,
createdAt: event.createdAt,
promptDispatched: event.promptDispatched === true,
dispatchedAsCommand: event.dispatchedAsCommand === true,
...(event.projectID ? { projectId: event.projectID } : {}),
...(event.title ? { title: event.title } : {}),
},
});
} catch {
uiOpenChamberEventClients.delete(client);
}
}
};
const scheduledTaskService = createScheduledTaskService({
readSettingsFromDiskMigrated,
sanitizeProjects,
projectConfigRuntime,
scheduledTasksRuntime,
});
const openChamberSessionService = createOpenChamberSessionService({
readSettingsFromDiskMigrated,
sanitizeProjects,
validateDirectoryPath,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
emitSessionCreatedEvent,
});
const openChamberControlService = createOpenChamberControlService({
readSettingsFromDiskMigrated,
sanitizeProjects,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
sessionService: openChamberSessionService,
scheduledTaskService,
});
const ensureGlobalWatcherStarted = async () => { const ensureGlobalWatcherStarted = async () => {
if (globalWatcherStartPromise) { if (globalWatcherStartPromise) {
@@ -1173,6 +1228,18 @@ async function main(options = {}) {
|| (typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0 || (typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0
? process.env.OPENCHAMBER_HOST.trim() ? process.env.OPENCHAMBER_HOST.trim()
: '127.0.0.1'); : '127.0.0.1');
agentToolRuntime = createAgentToolRuntime({
crypto,
fsPromises,
path,
dataDir: OPENCHAMBER_DATA_DIR,
env: process.env,
executeAction: (...args) => openChamberControlService.execute(...args),
getActivePort: () => {
const address = server?.address?.();
return typeof address === 'object' && address ? address.port : null;
},
});
// Pairing transports advertised to the create-device dialog. LAN reachability is // Pairing transports advertised to the create-device dialog. LAN reachability is
// derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP; // derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;
@@ -1455,6 +1522,7 @@ async function main(options = {}) {
fetchFreeZenModels, fetchFreeZenModels,
getCachedZenModels, getCachedZenModels,
setAutoAcceptSession, setAutoAcceptSession,
agentToolRuntime,
}); });
uiAuthController = bootstrapResult.uiAuthController; uiAuthController = bootstrapResult.uiAuthController;
realtimeProxyRuntime = attachRealtimeProxy({ realtimeProxyRuntime = attachRealtimeProxy({
@@ -1531,6 +1599,11 @@ async function main(options = {}) {
buildAugmentedPath, buildAugmentedPath,
projectConfigRuntime, projectConfigRuntime,
scheduledTasksRuntime, scheduledTasksRuntime,
scheduledTaskService,
openChamberSessionService,
openChamberControlService,
waitForOpenCodeReady,
emitSessionCreatedEvent,
getOpenChamberEventClients: () => uiOpenChamberEventClients, getOpenChamberEventClients: () => uiOpenChamberEventClients,
writeSseEvent, writeSseEvent,
permissionAutoAcceptRuntime, permissionAutoAcceptRuntime,
@@ -0,0 +1,90 @@
# Managed OpenChamber Agent Tool
## Purpose
This module exposes OpenChamber orchestration to agents as one typed OpenCode
custom tool named `openchamber`. It is injected only when OpenChamber launches
and owns the OpenCode process, and only while the persisted
`agentControlToolEnabled` setting is not `false` (default on; toggled in
Settings → General → OpenCode CLI and applied on the next managed OpenCode
restart).
## Runtime flow
1. The OpenChamber HTTP listener binds and publishes its authoritative port.
2. `prepareManagedOpenCodeEnv()` materializes the plugin under
`<openchamber-data-dir>/agent-tool/` and appends its `file://` URL to
`OPENCODE_CONFIG_CONTENT` without replacing existing plugin entries.
3. A random per-child token and loopback callback URL are added only to the
managed OpenCode child environment.
4. The plugin calls `POST /api/openchamber/agent-tool` with its typed input and
OpenCode's authoritative session directory.
5. The route delegates the fixed action allowlist directly to the shared
OpenChamber control service. The CLI uses the same service through its
authenticated HTTP adapter, so Goal Mode ordering, wait behavior,
partial-failure reporting, and scheduled-task contracts have one owner.
6. Each action definition owns a short presentation title and a separate
agent-facing description. The generated schema uses the description to state
required inputs or one non-obvious behavior, while completed calls use the
short title in native tool metadata.
## Agent context budget
- The tool exposes one shared parameter object rather than repeating parameters
in a large per-action union. Action descriptions carry only required inputs,
defaults, or one non-obvious semantic detail.
- Obvious fields rely on their names and JSON types. Parameter descriptions are
reserved for formats, dependencies, scope, and behavior that cannot be safely
inferred from the field name.
- Session dispatches do not wait by default. Agents are told to set `wait` only
when the user asks or the next step requires the completed result.
- The tool exposes only agent-relevant actions
(`OPENCHAMBER_AGENT_TOOL_ACTIONS`): `schedule.status` stays CLI-only because
`schedule.list` already returns scheduler status, and enable/disable are one
`schedule.toggle` action driven by the `disabled` boolean.
- The tool description frames intent: created sessions and scheduled tasks are
user-facing work the user follows up with, never a channel for the agent to
delegate parts of its own current task.
- Optional behavior switches (`worktree`, `goal`, `agent`, `variant`, `wait`)
state their default and an explicit "only when the user asks" rule so agents
do not invent worktrees, goal mode, or waits the user never requested.
- Detailed combination rules are enforced by the shared control service and
returned as actionable usage errors only after an invalid call. Per-action
examples and a repeated per-action parameter schema are intentionally omitted.
## Security invariants
- The callback accepts loopback requests only and requires the current
per-child bearer token using a timing-safe comparison.
- The token is never persisted, logged, returned to the UI, or written into
the materialized plugin.
- Inputs map to a fixed action and parameter allowlist. There is no arbitrary
CLI, shell, route, or URL forwarding.
- Session/worktree deletion and project-path registration are not exposed.
- An aborted tool request propagates an abort signal into the shared service.
## Result contract
Every completed call returns JSON:
```json
{
"schemaVersion": 1,
"ok": true,
"action": "session.create",
"data": {}
}
```
Command and operational failures use the same envelope with `ok: false` and
an `error` object. OpenCode-level cancellation can still produce a native tool
error state.
## Runtime parity
- Web and Desktop managed OpenCode: injected automatically.
- External OpenCode selected with `OPENCODE_HOST` or skip-start: not injected,
because OpenChamber does not control that process environment.
- VS Code: not injected; the extension owns a separate OpenCode lifecycle.
- Hosted and Capacitor mobile clients use the server's managed OpenCode tool
when connected to such a server; no tool runs in the client runtime.
@@ -0,0 +1,256 @@
import { parse as parseJsonc } from 'jsonc-parser';
import { pathToFileURL } from 'node:url';
import {
OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS,
OPENCHAMBER_AGENT_TOOL_ACTIONS,
} from '../openchamber-control/actions.js';
const TOOL_SCHEMA_VERSION = 1;
const ACTIONS = new Set(OPENCHAMBER_AGENT_TOOL_ACTIONS);
const AGENT_TOOL_ACTION_TITLES = Object.fromEntries(
OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS.map(({ action, title }) => [action, title]),
);
const PLUGIN_PARAMETER_PROPERTIES = {
projectId: { type: 'string', description: 'Configured project ID; do not combine with directory' },
directory: { type: 'string', description: 'Absolute checkout or session directory; defaults to the current session directory' },
sessionId: { type: 'string' },
messageId: { type: 'string', description: 'Optional fork boundary message ID' },
taskId: { type: 'string' },
title: { type: 'string' },
prompt: { type: 'string' },
model: { type: 'string', description: 'Model in provider/model format. When the user names no model: for session.create pick a suitable one from models.list favorites or recents (omit if there are none); for send and fork omit it — the session reuses its previous model' },
agent: { type: 'string', description: 'OpenCode agent name; new sessions default to the build agent and existing sessions keep their previous one. Set only when the user explicitly requests a different agent' },
variant: { type: 'string', description: 'Model variant; use only when the user explicitly requests it' },
worktree: { type: 'string', description: 'New worktree name for session.create. Omit by default; use only when the user explicitly asks for an isolated worktree. Uncommitted changes do not carry over into a new worktree' },
branch: { type: 'string', description: 'Branch name for the new worktree' },
startRef: { type: 'string', description: 'Git ref used to create the new worktree' },
setUpstream: { type: 'boolean', description: 'Make the new worktree branch track its upstream' },
goal: { type: 'boolean', description: 'Run the dispatched prompt in Goal Mode; use only when the user explicitly requests it' },
goalTokenBudget: { type: 'integer', minimum: 1000, maximum: 100_000_000, description: 'Goal token budget; requires goal' },
wait: { type: 'boolean', description: 'Wait for current session activity to become idle. Omit by default; use only when the user asks or the next step requires the completed result' },
timeout: { type: 'integer', minimum: 1, maximum: 86_400, description: 'Wait timeout in seconds (default 600); requires wait' },
lastAssistant: { type: 'boolean', description: 'Return the last assistant text; create/send/fork require wait' },
limit: { type: 'integer', minimum: 1, description: 'Maximum sessions or messages to return (default 10)' },
all: { type: 'boolean', description: 'Include archived sessions or all messages, depending on the action' },
last: { type: 'boolean', description: 'Return only the last matching session message' },
withStatus: { type: 'boolean', description: 'Include authoritative status in session.list' },
role: { type: 'string', enum: ['all', 'user', 'assistant'], description: 'Message role filter' },
name: { type: 'string' },
daily: { type: 'string', description: 'Daily run time in HH:mm format' },
weekly: { type: 'string', description: 'Comma-separated weekdays; 0=Sunday and 6=Saturday' },
once: { type: 'string', description: 'One-time run date in YYYY-MM-DD format' },
time: { type: 'string', description: 'Weekly or one-time run time in HH:mm format' },
cron: { type: 'string', description: 'Cron expression' },
timezone: { type: 'string', description: 'IANA timezone' },
disabled: { type: 'boolean', description: 'true disables and false enables; required for schedule.toggle' },
};
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const createResult = ({ ok, action, data, error, exitCode }) => ({
schemaVersion: TOOL_SCHEMA_VERSION,
ok,
action: action || 'unknown',
...(data !== undefined ? { data } : {}),
...(error ? { error } : {}),
...(Number.isInteger(exitCode) ? { exitCode } : {}),
});
const isLoopbackAddress = (value) => {
const address = typeof value === 'string' ? value.toLowerCase() : '';
return address === '127.0.0.1'
|| address === '::1'
|| address === '::ffff:127.0.0.1';
};
const createPluginSource = () => String.raw`
export const OpenChamberPlugin = async () => ({
tool: {
openchamber: {
description: "Control OpenChamber projects, sessions, and scheduled tasks on the user's behalf. Sessions and scheduled tasks you create are for the user to follow and interact with; never use this tool to delegate parts of your own current task. Use one action per call. Scope with projectId or directory; omit both to use the current session directory. Session dispatches return immediately by default and you receive no notification when a dispatched session finishes, so never promise to report back on it; the user follows it in OpenChamber; a dispatched session needs no follow-up from you. If the user later asks how it went, use session.messages (add wait to block until it is idle, lastAssistant for just the final answer) — session.send always sends a NEW prompt and never just waits. Set wait only when the user asks or the next step requires the completed result. Session and worktree deletion are unavailable.",
args: {
action: { type: "string", enum: ${JSON.stringify(OPENCHAMBER_AGENT_TOOL_ACTIONS)}, oneOf: ${JSON.stringify(OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS.map(({ action, description }) => ({ const: action, description })))}, description: "OpenChamber action to perform" },
parameters: { type: "object", properties: ${JSON.stringify(PLUGIN_PARAMETER_PROPERTIES)}, additionalProperties: false, description: "Inputs for the action; use an empty object when none are needed" },
},
async execute(input, context) {
const args = { ...(input.parameters ?? {}), action: input.action }
const actionTitles = ${JSON.stringify(AGENT_TOOL_ACTION_TITLES)}
const title = Object.hasOwn(actionTitles, args.action) ? actionTitles[args.action] : args.action
context.metadata({
title,
metadata: {
openchamber: {
schemaVersion: ${TOOL_SCHEMA_VERSION},
action: args.action,
description: title,
},
},
})
const endpoint = process.env.OPENCHAMBER_AGENT_TOOL_URL
const token = process.env.OPENCHAMBER_AGENT_TOOL_TOKEN
const failure = (payload) => ({
title,
output: JSON.stringify(payload),
metadata: { openchamber: { schemaVersion: ${TOOL_SCHEMA_VERSION}, action: args.action, description: title, ok: false } },
})
if (!endpoint || !token) {
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: "OpenChamber managed tool connection is unavailable" } })
}
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
authorization: "Bearer " + token,
"content-type": "application/json",
},
body: JSON.stringify({ input: args, contextDirectory: context.directory }),
signal: context.abort,
})
const output = await response.text()
let result = null
try { result = JSON.parse(output) } catch {}
const valid = result?.schemaVersion === ${TOOL_SCHEMA_VERSION} && typeof result?.ok === "boolean" && typeof result?.action === "string"
context.metadata({
title,
metadata: {
openchamber: {
schemaVersion: ${TOOL_SCHEMA_VERSION},
action: args.action,
description: title,
ok: valid && result.ok === true,
},
},
})
if (valid) return { title, output, metadata: { openchamber: { schemaVersion: ${TOOL_SCHEMA_VERSION}, action: args.action, description: title, ok: result.ok === true } } }
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: "OpenChamber returned an invalid response", kind: "runtime", status: response.status } })
} catch (error) {
if (context.abort.aborted) throw error
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: error instanceof Error ? error.message : String(error), kind: "runtime" } })
}
},
},
},
})
`;
const mergePluginConfig = (rawConfig, pluginUrl) => {
const errors = [];
const parsed = asNonEmptyString(rawConfig) ? parseJsonc(rawConfig, errors, { allowTrailingComma: true }) : {};
if (errors.length > 0 || !parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('OPENCODE_CONFIG_CONTENT must contain a valid JSON object before OpenChamber can inject its managed tool');
}
if (parsed.plugin !== undefined && !Array.isArray(parsed.plugin)) {
throw new Error('OPENCODE_CONFIG_CONTENT plugin must be an array before OpenChamber can inject its managed tool');
}
const configured = Array.isArray(parsed.plugin) ? parsed.plugin : [];
parsed.plugin = [
...configured.filter((value) => value !== pluginUrl && (!Array.isArray(value) || value[0] !== pluginUrl)),
pluginUrl,
];
return JSON.stringify(parsed);
};
export const createAgentToolRuntime = (dependencies) => {
const {
crypto,
fsPromises,
path,
dataDir,
getActivePort,
executeAction,
env = process.env,
} = dependencies;
const pluginDirectory = path.join(dataDir, 'agent-tool');
const pluginPath = path.join(pluginDirectory, 'openchamber-plugin.js');
let activeToken = null;
const prepareManagedOpenCodeEnv = async () => {
const port = getActivePort();
if (!Number.isInteger(port) || port <= 0) {
throw new Error('OpenChamber listener port is unavailable for managed tool injection');
}
await fsPromises.mkdir(pluginDirectory, { recursive: true });
await fsPromises.writeFile(pluginPath, createPluginSource(), { mode: 0o600 });
activeToken = crypto.randomBytes(32).toString('base64url');
const pluginUrl = pathToFileURL(pluginPath).href;
return {
OPENCODE_CONFIG_CONTENT: mergePluginConfig(env.OPENCODE_CONFIG_CONTENT, pluginUrl),
OPENCHAMBER_AGENT_TOOL_URL: `http://127.0.0.1:${port}/api/openchamber/agent-tool`,
OPENCHAMBER_AGENT_TOOL_TOKEN: activeToken,
};
};
const authorize = (req) => {
if (!activeToken || !isLoopbackAddress(req.socket?.remoteAddress)) return false;
const header = asNonEmptyString(req.headers?.authorization);
if (!header?.startsWith('Bearer ')) return false;
const provided = Buffer.from(header.slice(7));
const expected = Buffer.from(activeToken);
return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
};
const execute = async (payload = {}, options = {}) => {
const action = asNonEmptyString(payload.input?.action);
if (!action || !ACTIONS.has(action)) {
return createResult({ ok: false, action, error: { message: `Unsupported OpenChamber action: ${action || 'missing'}`, kind: 'usage' } });
}
if (typeof executeAction !== 'function') {
return createResult({ ok: false, action, error: { message: 'OpenChamber control service is unavailable', kind: 'runtime' } });
}
try {
const data = await executeAction(action, payload.input, payload.contextDirectory, options);
return createResult({ ok: true, action, data });
} catch (error) {
return createResult({
ok: false,
action,
...(error?.partial === true ? { data: {
partial: true,
partialAction: error.partialAction,
sessionId: error.sessionId,
directory: error.directory,
} } : {}),
error: {
message: error instanceof Error ? error.message : String(error),
kind: Number(error?.statusCode) >= 400 && Number(error?.statusCode) < 499 ? 'usage' : 'runtime',
},
});
}
};
const registerRoutes = (app, express) => {
app.post('/api/openchamber/agent-tool', express.json({ limit: '1mb' }), async (req, res) => {
if (!authorize(req)) return res.status(401).json({ error: 'Unauthorized' });
const controller = new AbortController();
const abortOnDisconnect = () => {
if (!res.writableEnded) controller.abort();
};
req.once('aborted', abortOnDisconnect);
res.once('close', abortOnDisconnect);
try {
return res.json(await execute(req.body, { signal: controller.signal }));
} catch (error) {
return res.json(createResult({
ok: false,
action: req.body?.input?.action,
error: { message: error instanceof Error ? error.message : String(error), kind: 'runtime' },
}));
} finally {
req.off('aborted', abortOnDisconnect);
res.off('close', abortOnDisconnect);
}
});
};
return {
prepareManagedOpenCodeEnv,
registerRoutes,
execute,
};
};
@@ -0,0 +1,236 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { pathToFileURL } from 'node:url';
import express from 'express';
import request from 'supertest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createAgentToolRuntime } from './runtime.js';
import { OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS, OPENCHAMBER_CONTROL_ACTION_DEFINITIONS } from '../openchamber-control/actions.js';
const temporaryDirectories = [];
afterEach(async () => {
await Promise.all(temporaryDirectories.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true })));
});
const createRuntime = async (overrides = {}) => {
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openchamber-agent-tool-'));
temporaryDirectories.push(dataDir);
const executeAction = vi.fn(async () => ({ projects: [] }));
const env = {};
const runtime = createAgentToolRuntime({
crypto,
fsPromises: fs,
path,
dataDir,
getActivePort: () => 3901,
executeAction,
env,
...overrides,
});
return { runtime, dataDir, executeAction, env };
};
describe('agent tool action allowlist', () => {
it('defines a short title and agent description for every action', () => {
expect(OPENCHAMBER_CONTROL_ACTION_DEFINITIONS.every(({ action, title, description }) => action && title && description)).toBe(true);
});
it.each([
'projects.list',
'models.list',
'session.list',
'session.create',
'session.send',
'session.fork',
'session.status',
'session.messages',
'schedule.list',
'schedule.create',
'schedule.run',
'schedule.delete',
'schedule.toggle',
])('delegates %s to the shared control service', async (action) => {
const { runtime, executeAction } = await createRuntime();
const input = { action, projectId: 'project-1' };
await runtime.execute({ input, contextDirectory: '/work/project' });
expect(executeAction).toHaveBeenCalledWith(action, input, '/work/project', {});
});
it.each([
'session.delete',
'schedule.status',
])('rejects %s outside the agent allowlist without invoking the service', async (action) => {
const { runtime, executeAction } = await createRuntime();
await expect(runtime.execute({ input: { action } })).resolves.toEqual(expect.objectContaining({
ok: false,
action,
error: expect.objectContaining({ kind: 'usage' }),
}));
expect(executeAction).not.toHaveBeenCalled();
});
});
describe('managed agent tool runtime', () => {
it('materializes the plugin and preserves configured plugin entries', async () => {
const { runtime, dataDir, env } = await createRuntime();
env.OPENCODE_CONFIG_CONTENT = '{ // existing\n "plugin": ["file:///existing.js", ["example-plugin", {"flag": true}]], "model": "test/model" }';
const preparedEnv = await runtime.prepareManagedOpenCodeEnv();
const config = JSON.parse(preparedEnv.OPENCODE_CONFIG_CONTENT);
const pluginPath = path.join(dataDir, 'agent-tool', 'openchamber-plugin.js');
const source = await fs.readFile(pluginPath, 'utf8');
expect(config.model).toBe('test/model');
expect(config.plugin).toEqual([
'file:///existing.js',
['example-plugin', { flag: true }],
expect.stringContaining('/agent-tool/openchamber-plugin.js'),
]);
expect(preparedEnv.OPENCHAMBER_AGENT_TOOL_URL).toBe('http://127.0.0.1:3901/api/openchamber/agent-tool');
expect(preparedEnv.OPENCHAMBER_AGENT_TOOL_TOKEN).toMatch(/^[A-Za-z0-9_-]+$/);
expect(source).toContain('openchamber: {');
for (const { action, description } of OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS) {
expect(source).toContain(JSON.stringify({ const: action, description }));
}
expect(source).not.toContain('"schedule.status"');
const pluginModule = await import(`${pathToFileURL(pluginPath).href}?schema=${Date.now()}`);
const hooks = await pluginModule.OpenChamberPlugin();
expect(hooks.tool.openchamber.description).toContain('Session dispatches return immediately by default');
expect(hooks.tool.openchamber.description).toContain('Set wait only when the user asks or the next step requires the completed result');
expect(hooks.tool.openchamber.args.action.oneOf).toContainEqual({
const: 'session.messages',
description: 'Read text-only messages and current sessionStatus for sessionId; directory and limit 10 are defaults',
});
expect(hooks.tool.openchamber.args.parameters.properties.wait.description).toBe(
'Wait for current session activity to become idle. Omit by default; use only when the user asks or the next step requires the completed result',
);
expect(hooks.tool.openchamber.args.parameters.properties.sessionId).toEqual({ type: 'string' });
expect(source).not.toContain('title: "OpenChamber"');
expect(source).not.toContain('@opencode-ai/plugin');
expect(source).not.toContain(preparedEnv.OPENCHAMBER_AGENT_TOOL_TOKEN);
});
it('executes actions through the shared control service', async () => {
const executeAction = vi.fn(async () => ({ projects: [] }));
const { runtime } = await createRuntime({ executeAction });
const result = await runtime.execute({
input: { action: 'projects.list' },
contextDirectory: '/work/project',
});
expect(result).toEqual({
schemaVersion: 1,
ok: true,
action: 'projects.list',
data: { projects: [] },
});
expect(executeAction).toHaveBeenCalledWith('projects.list', { action: 'projects.list' }, '/work/project', {});
});
it('keeps service failures as structured tool results', async () => {
const error = Object.assign(new Error('Task not found'), { statusCode: 404 });
const { runtime } = await createRuntime({ executeAction: vi.fn(async () => { throw error; }) });
await expect(runtime.execute({
input: { action: 'schedule.run', taskId: 'missing' },
contextDirectory: '/work/project',
})).resolves.toEqual(expect.objectContaining({
schemaVersion: 1,
ok: false,
action: 'schedule.run',
error: { message: 'Task not found', kind: 'usage' },
}));
});
it('forwards cancellation to the shared control service', async () => {
const executeAction = vi.fn(async (_action, _input, _directory, options) => {
await new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => reject(Object.assign(new Error('OpenChamber action was cancelled'), { statusCode: 499 })), { once: true });
});
});
const { runtime } = await createRuntime({ executeAction });
const controller = new AbortController();
const pending = runtime.execute({ input: { action: 'projects.list' } }, { signal: controller.signal });
controller.abort();
await expect(pending).resolves.toEqual(expect.objectContaining({
ok: false,
action: 'projects.list',
error: { message: 'OpenChamber action was cancelled', kind: 'runtime' },
}));
expect(executeAction).toHaveBeenCalledWith('projects.list', { action: 'projects.list' }, undefined, { signal: controller.signal });
});
it('requires the per-child token on the loopback route', async () => {
const { runtime } = await createRuntime();
const env = await runtime.prepareManagedOpenCodeEnv();
const app = express();
runtime.registerRoutes(app, express);
await request(app)
.post('/api/openchamber/agent-tool')
.send({ input: { action: 'projects.list' } })
.expect(401);
const response = await request(app)
.post('/api/openchamber/agent-tool')
.set('authorization', `Bearer ${env.OPENCHAMBER_AGENT_TOOL_TOKEN}`)
.send({ input: { action: 'projects.list' } })
.expect(200);
expect(response.body).toEqual(expect.objectContaining({ ok: true, action: 'projects.list' }));
});
it('executes through the materialized plugin and authenticated callback', async () => {
let activePort = null;
const { runtime, dataDir } = await createRuntime({ getActivePort: () => activePort });
const app = express();
runtime.registerRoutes(app, express);
const server = await new Promise((resolve) => {
const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
});
activePort = server.address().port;
const previousUrl = process.env.OPENCHAMBER_AGENT_TOOL_URL;
const previousToken = process.env.OPENCHAMBER_AGENT_TOOL_TOKEN;
try {
const env = await runtime.prepareManagedOpenCodeEnv();
process.env.OPENCHAMBER_AGENT_TOOL_URL = env.OPENCHAMBER_AGENT_TOOL_URL;
process.env.OPENCHAMBER_AGENT_TOOL_TOKEN = env.OPENCHAMBER_AGENT_TOOL_TOKEN;
const pluginPath = path.join(dataDir, 'agent-tool', 'openchamber-plugin.js');
const pluginModule = await import(`${pathToFileURL(pluginPath).href}?test=${Date.now()}`);
const hooks = await pluginModule.OpenChamberPlugin();
const metadata = vi.fn();
const result = await hooks.tool.openchamber.execute(
{ action: 'projects.list', parameters: {} },
{ directory: '/work/project', abort: new AbortController().signal, metadata },
);
expect(JSON.parse(result.output)).toEqual({
schemaVersion: 1,
ok: true,
action: 'projects.list',
data: { projects: [] },
});
expect(result.title).toBe('List configured projects');
expect(result.metadata.openchamber.description).toBe('List configured projects');
expect(metadata).toHaveBeenCalledWith(expect.objectContaining({
title: 'List configured projects',
metadata: expect.objectContaining({
openchamber: expect.objectContaining({ description: 'List configured projects' }),
}),
}));
} finally {
if (previousUrl === undefined) delete process.env.OPENCHAMBER_AGENT_TOOL_URL;
else process.env.OPENCHAMBER_AGENT_TOOL_URL = previousUrl;
if (previousToken === undefined) delete process.env.OPENCHAMBER_AGENT_TOOL_TOKEN;
else process.env.OPENCHAMBER_AGENT_TOOL_TOKEN = previousToken;
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
});
@@ -0,0 +1,47 @@
# OpenChamber Control Service
## Purpose
This module owns the typed control contract shared by the OpenChamber CLI and
the managed OpenCode `openchamber` tool. Both adapters delegate to
`createOpenChamberControlService()`; neither adapter may call or spawn the
other.
## Boundaries
- `service.js` validates and executes the fixed project, model, session, and
scheduled-task action allowlist. `actions.js` marks CLI-only actions with
`agentExposed: false` (currently `schedule.status`); the agent tool consumes
the filtered `OPENCHAMBER_AGENT_TOOL_*` exports. `schedule.toggle` requires
the `disabled` boolean and replaces separate enable/disable actions;
`schedule.list` also returns scheduler status as `scheduler`.
- `routes.js` is the authenticated CLI HTTP adapter. It forwards one action,
preserves service status and partial-result details, and propagates request
cancellation.
- `../agent-tool/runtime.js` is the managed-tool adapter. It wraps service
results in the versioned native-tool envelope and uses a separate ephemeral
loopback credential.
- `../openchamber-sessions/routes.js` and `../scheduled-tasks/service.js` own
their domain operations and are composed into this service.
## Invariants
- Session status and messages come from official directory-scoped OpenCode
APIs. Message output includes only ordered `text` parts.
- Wait never treats an initial idle response as completion after dispatch. It
requires observed activity or a newly completed assistant message.
- Timeout and cancellation are failures, never authoritative idle results.
- Validation that protects side effects runs before session creation or
dispatch.
- Send and fork dispatches without an explicit model/agent/variant reuse the
target session's last user-message selection before falling back to the
configured defaults; only session creation resolves defaults directly.
- Usage errors name the missing or conflicting input so CLI and agent-tool
callers can correct an invalid request without an upfront usage manual.
- Explicit `projectId` or `directory` scope takes precedence over the managed
tool's current-session directory fallback; the fallback never creates a
conflicting second scope.
- One failed directory status lookup produces `unknown` for only that
directory and does not erase other session results.
- Destructive session/worktree deletion and project-path registration are not
part of the action contract.
@@ -0,0 +1,28 @@
export const OPENCHAMBER_CONTROL_ACTION_DEFINITIONS = Object.freeze([
{ action: 'projects.list', title: 'List configured projects', description: 'List configured projects; no parameters' },
{ action: 'models.list', title: 'Show model preferences', description: 'Show default, favorite, and recent model preferences; no parameters' },
{ action: 'session.list', title: 'List sessions', description: 'List sessions; optional directory, limit (default 10), all, or withStatus' },
{ action: 'session.create', title: 'Create a session', description: 'Create a session in the current directory by default; prompt is optional' },
{ action: 'session.send', title: 'Send a prompt', description: 'Send a new prompt to sessionId; scope with projectId or directory' },
{ action: 'session.fork', title: 'Fork a session', description: 'Fork sessionId; messageId selects the boundary; prompt is optional' },
{ action: 'session.status', title: 'Check session status', description: 'Check sessionId status; directory defaults to the current session' },
{ action: 'session.messages', title: 'Read session messages', description: 'Read text-only messages and current sessionStatus for sessionId; directory and limit 10 are defaults' },
{ action: 'schedule.status', title: 'Check scheduler status', description: 'Check scheduler status; no parameters', agentExposed: false },
{ action: 'schedule.list', title: 'List scheduled tasks', description: 'List tasks and scheduler status; scope with projectId or directory' },
{ action: 'schedule.create', title: 'Create a scheduled task', description: 'Create task; requires name, prompt, model, and one schedule selector' },
{ action: 'schedule.run', title: 'Run a scheduled task', description: 'Run taskId; scope with projectId or directory' },
{ action: 'schedule.delete', title: 'Delete a scheduled task', description: 'Delete taskId; scope with projectId or directory' },
{ action: 'schedule.toggle', title: 'Enable or disable a scheduled task', description: 'Enable or disable taskId; requires the disabled boolean' },
]);
export const OPENCHAMBER_CONTROL_ACTIONS = Object.freeze(
OPENCHAMBER_CONTROL_ACTION_DEFINITIONS.map(({ action }) => action),
);
export const OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS = Object.freeze(
OPENCHAMBER_CONTROL_ACTION_DEFINITIONS.filter(({ agentExposed }) => agentExposed !== false),
);
export const OPENCHAMBER_AGENT_TOOL_ACTIONS = Object.freeze(
OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS.map(({ action }) => action),
);
@@ -0,0 +1,16 @@
export class OpenChamberControlError extends Error {
constructor(message, statusCode = 500, details = {}) {
super(message);
this.name = 'OpenChamberControlError';
this.statusCode = statusCode;
Object.assign(this, details);
}
}
export const asControlError = (error, fallbackMessage, fallbackStatus = 500) => {
if (error instanceof OpenChamberControlError) return error;
const message = error instanceof Error ? error.message : fallbackMessage;
return new OpenChamberControlError(message || fallbackMessage, Number(error?.statusCode) || fallbackStatus, {
...(error?.goalConfigured === true ? { goalConfigured: true } : {}),
});
};
@@ -0,0 +1,36 @@
import express from 'express';
import { asControlError } from './error.js';
export const registerOpenChamberControlRoutes = (app, { controlService }) => {
app.post('/api/openchamber/control', express.json({ limit: '1mb' }), async (req, res) => {
const controller = new AbortController();
const abortOnDisconnect = () => {
if (!res.writableEnded) controller.abort();
};
req.once('aborted', abortOnDisconnect);
res.once('close', abortOnDisconnect);
try {
const action = typeof req.body?.action === 'string' ? req.body.action : '';
const requestInput = req.body?.input;
const input = requestInput && typeof requestInput === 'object' && !Array.isArray(requestInput)
? requestInput
: {};
const data = await controlService.execute(action, input, req.body?.contextDirectory, { signal: controller.signal });
return res.json(data);
} catch (error) {
const controlError = asControlError(error, 'OpenChamber control action failed');
return res.status(controlError.statusCode).json({
error: controlError.message,
...(controlError.partial === true ? {
partial: true,
partialAction: controlError.partialAction,
sessionId: controlError.sessionId,
directory: controlError.directory,
} : {}),
});
} finally {
req.off('aborted', abortOnDisconnect);
res.off('close', abortOnDisconnect);
}
});
};
@@ -0,0 +1,46 @@
import express from 'express';
import request from 'supertest';
import { describe, expect, it, vi } from 'vitest';
import { OpenChamberControlError } from './error.js';
import { registerOpenChamberControlRoutes } from './routes.js';
const createApp = (execute) => {
const app = express();
registerOpenChamberControlRoutes(app, { controlService: { execute } });
return app;
};
describe('OpenChamber control route', () => {
it('is a thin adapter over the control service', async () => {
const execute = vi.fn(async () => ({ projects: [] }));
const response = await request(createApp(execute))
.post('/api/openchamber/control')
.send({ action: 'projects.list', input: {}, contextDirectory: '/repo' })
.expect(200);
expect(response.body).toEqual({ projects: [] });
expect(execute).toHaveBeenCalledWith('projects.list', {}, '/repo', expect.objectContaining({ signal: expect.any(AbortSignal) }));
});
it('preserves service status and partial-result details', async () => {
const execute = vi.fn(async () => {
throw new OpenChamberControlError('dispatch failed', 500, {
partial: true,
partialAction: 'fork-created',
sessionId: 'ses_fork',
directory: '/repo',
});
});
const response = await request(createApp(execute))
.post('/api/openchamber/control')
.send({ action: 'session.fork', input: {} })
.expect(500);
expect(response.body).toEqual({
error: 'dispatch failed',
partial: true,
partialAction: 'fork-created',
sessionId: 'ses_fork',
directory: '/repo',
});
});
});
@@ -0,0 +1,398 @@
import path from 'node:path';
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
import { OpenChamberControlError, asControlError } from './error.js';
import { OPENCHAMBER_CONTROL_ACTIONS } from './actions.js';
const DEFAULT_WAIT_TIMEOUT_SECONDS = 600;
const MAX_WAIT_TIMEOUT_SECONDS = 86_400;
const WAIT_POLL_INTERVAL_MS = 500;
const CONTROL_ACTIONS = new Set(OPENCHAMBER_CONTROL_ACTIONS);
const SCHEDULE_TASK_ID_ACTIONS = new Set([
'schedule.run',
'schedule.delete',
'schedule.toggle',
]);
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const positiveInteger = (value, fallback, field) => {
if (value === undefined || value === null) return fallback;
const number = Number(value);
if (!Number.isSafeInteger(number) || number < 1) {
throw new OpenChamberControlError(`${field} must be a positive integer`, 400);
}
return number;
};
const normalizeWaitTimeoutMs = (value) => {
const seconds = value === undefined || value === null ? DEFAULT_WAIT_TIMEOUT_SECONDS : Number(value);
if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > MAX_WAIT_TIMEOUT_SECONDS) {
throw new OpenChamberControlError(`timeout must be from 1 to ${MAX_WAIT_TIMEOUT_SECONDS} seconds`, 400);
}
return seconds * 1000;
};
const extractTextMessages = (messages, role = 'all') => {
const result = [];
for (const record of Array.isArray(messages) ? messages : []) {
const info = record?.info;
const messageRole = info?.role;
if ((messageRole !== 'user' && messageRole !== 'assistant') || (role !== 'all' && role !== messageRole)) continue;
const text = Array.isArray(record?.parts)
? record.parts.filter((part) => part?.type === 'text' && typeof part.text === 'string').map((part) => part.text).join('').trim()
: '';
if (!text) continue;
const providerID = asNonEmptyString(info.providerID);
const modelID = asNonEmptyString(info.modelID);
result.push({
id: asNonEmptyString(info.id) || '',
role: messageRole,
createdAt: Number.isFinite(info?.time?.created) ? info.time.created : null,
completedAt: Number.isFinite(info?.time?.completed) ? info.time.completed : null,
model: providerID && modelID ? `${providerID}/${modelID}` : null,
text,
});
}
return result.sort((left, right) => (left.createdAt || 0) - (right.createdAt || 0));
};
const parseModel = (value) => {
const model = asNonEmptyString(value);
if (!model) throw new OpenChamberControlError('model is required', 400);
const slashIndex = model.indexOf('/');
if (slashIndex <= 0 || slashIndex === model.length - 1) {
throw new OpenChamberControlError('model must be in provider/model format', 400);
}
return { providerID: model.slice(0, slashIndex), modelID: model.slice(slashIndex + 1) };
};
const parseWeekdays = (value) => {
const raw = asNonEmptyString(value);
if (!raw) throw new OpenChamberControlError('weekly is required', 400);
const weekdays = raw.split(',').map((entry) => Number.parseInt(entry.trim(), 10));
if (weekdays.some((entry) => !Number.isInteger(entry) || entry < 0 || entry > 6)) {
throw new OpenChamberControlError('weekly must contain weekdays from 0 to 6', 400);
}
return Array.from(new Set(weekdays)).sort((a, b) => a - b);
};
const buildSchedule = (input) => {
const daily = asNonEmptyString(input.daily);
const weekly = asNonEmptyString(input.weekly);
const once = asNonEmptyString(input.once);
const cron = asNonEmptyString(input.cron);
const selectors = [daily, weekly, once, cron].filter(Boolean);
if (selectors.length !== 1) {
throw new OpenChamberControlError('Provide exactly one of daily, weekly, once, or cron', 400);
}
const timezone = asNonEmptyString(input.timezone);
if (daily) return { kind: 'daily', times: [daily], ...(timezone ? { timezone } : {}) };
if (weekly) {
const time = asNonEmptyString(input.time);
if (!time) throw new OpenChamberControlError('time is required with weekly', 400);
return { kind: 'weekly', weekdays: parseWeekdays(weekly), times: [time], ...(timezone ? { timezone } : {}) };
}
if (once) {
const time = asNonEmptyString(input.time);
if (!time) throw new OpenChamberControlError('time is required with once', 400);
return { kind: 'once', date: once, time, ...(timezone ? { timezone } : {}) };
}
return { kind: 'cron', cron, ...(timezone ? { timezone } : {}) };
};
const buildScheduledTask = (input) => {
const name = asNonEmptyString(input.name);
const prompt = asNonEmptyString(input.prompt);
if (!name) throw new OpenChamberControlError('name is required', 400);
if (!prompt) throw new OpenChamberControlError('prompt is required', 400);
const model = parseModel(input.model);
const goalTokenBudget = input.goalTokenBudget;
if (goalTokenBudget !== undefined && input.goal !== true) {
throw new OpenChamberControlError('goalTokenBudget requires goal', 400);
}
if (goalTokenBudget !== undefined && (!Number.isSafeInteger(goalTokenBudget) || goalTokenBudget < 1000 || goalTokenBudget > 100_000_000)) {
throw new OpenChamberControlError('goalTokenBudget must be from 1000 to 100000000', 400);
}
return {
name,
enabled: input.disabled !== true,
schedule: buildSchedule(input),
execution: {
prompt,
...model,
...(asNonEmptyString(input.agent) ? { agent: input.agent.trim() } : {}),
...(asNonEmptyString(input.variant) ? { variant: input.variant.trim() } : {}),
...(input.goal === true ? { goalEnabled: true } : {}),
...(goalTokenBudget !== undefined ? { goalTokenBudget } : {}),
},
};
};
export const createOpenChamberControlService = (dependencies) => {
const {
readSettingsFromDiskMigrated,
sanitizeProjects,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
sessionService,
scheduledTaskService,
createClient = createOpencodeClient,
sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration)),
now = Date.now,
} = dependencies;
const wait = (duration, signal) => {
if (!signal) return sleep(duration);
if (signal.aborted) return Promise.reject(new OpenChamberControlError('OpenChamber action was cancelled', 499));
return new Promise((resolve, reject) => {
const onAbort = () => {
signal.removeEventListener('abort', onAbort);
reject(new OpenChamberControlError('OpenChamber action was cancelled', 499));
};
signal.addEventListener('abort', onAbort, { once: true });
sleep(duration).then(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, (error) => {
signal.removeEventListener('abort', onAbort);
reject(error);
});
});
};
const getClient = async () => {
if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
return createClient({
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
headers: getOpenCodeAuthHeaders(),
});
};
const projects = async () => {
const settings = await readSettingsFromDiskMigrated();
return sanitizeProjects(settings?.projects || []).map((project) => ({
id: project.id,
path: path.resolve(project.path),
label: asNonEmptyString(project.label) || path.basename(project.path) || project.path,
}));
};
const models = async () => {
const settings = await readSettingsFromDiskMigrated();
return {
defaultModel: asNonEmptyString(settings?.defaultModel),
defaultVariant: asNonEmptyString(settings?.defaultVariant),
defaultAgent: asNonEmptyString(settings?.defaultAgent),
favoriteModels: Array.isArray(settings?.favoriteModels) ? settings.favoriteModels : [],
recentModels: Array.isArray(settings?.recentModels) ? settings.recentModels : [],
};
};
const sessionStatus = async (client, sessionID, directory) => {
const response = await client.session.status({ directory });
const statuses = response?.data;
if (!statuses || typeof statuses !== 'object' || Array.isArray(statuses)) {
throw new OpenChamberControlError('Invalid session status response', 500);
}
return statuses[sessionID] || { type: 'idle' };
};
const sessionMessages = async (client, sessionID, directory, role, limit) => {
const fetchLimit = limit === undefined ? undefined : Math.max(100, limit * 4);
let response = await client.session.messages({ sessionID, directory, ...(fetchLimit ? { limit: fetchLimit } : {}) });
let raw = Array.isArray(response?.data) ? response.data : [];
let messages = extractTextMessages(raw, role);
if (limit !== undefined && messages.length < limit && raw.length >= fetchLimit) {
response = await client.session.messages({ sessionID, directory });
raw = Array.isArray(response?.data) ? response.data : [];
messages = extractTextMessages(raw, role);
}
return limit === undefined ? messages : messages.slice(-limit);
};
const waitForIdle = async ({ client, sessionID, directory, timeoutMs, requireActivity, baselineMessageID, startedAt, signal }) => {
const deadline = now() + timeoutMs;
let observedActivity = false;
while (true) {
if (signal?.aborted) throw new OpenChamberControlError('OpenChamber action was cancelled', 499);
const status = await sessionStatus(client, sessionID, directory);
if (status.type === 'busy' || status.type === 'retry') {
observedActivity = true;
} else if (!requireActivity || observedActivity) {
return status;
} else {
const messages = await sessionMessages(client, sessionID, directory, 'assistant', 1);
const message = messages[0];
if (message?.completedAt && (baselineMessageID ? message.id !== baselineMessageID : message.completedAt >= startedAt)) {
return status;
}
}
const remaining = deadline - now();
if (remaining <= 0) {
throw new OpenChamberControlError(`Session did not become idle within ${Math.ceil(timeoutMs / 1000)} seconds`, 500);
}
await wait(Math.min(WAIT_POLL_INTERVAL_MS, remaining), signal);
}
};
const executeSessionAction = async (action, input, contextDirectory, signal) => {
if (input.timeout !== undefined && input.wait !== true) throw new OpenChamberControlError('timeout requires wait', 400);
if (input.lastAssistant === true && input.wait !== true) throw new OpenChamberControlError('lastAssistant requires wait', 400);
const directory = asNonEmptyString(input.directory) || (!input.projectId ? asNonEmptyString(contextDirectory) : null);
const payload = {
...(directory ? { directory } : {}),
...(asNonEmptyString(input.projectId) ? { projectId: input.projectId.trim() } : {}),
...(asNonEmptyString(input.title) ? { title: input.title.trim() } : {}),
...(asNonEmptyString(input.prompt) ? { prompt: input.prompt.trim() } : {}),
...(asNonEmptyString(input.model) ? { model: input.model.trim() } : {}),
...(asNonEmptyString(input.agent) ? { agent: input.agent.trim() } : {}),
...(asNonEmptyString(input.variant) ? { variant: input.variant.trim() } : {}),
...(input.goal === true ? { goal: true } : {}),
...(input.goalTokenBudget !== undefined ? { goalTokenBudget: input.goalTokenBudget } : {}),
...(asNonEmptyString(input.worktree) ? { worktree: {
name: input.worktree.trim(),
...(asNonEmptyString(input.branch) ? { branchName: input.branch.trim() } : {}),
...(asNonEmptyString(input.startRef) ? { startRef: input.startRef.trim() } : {}),
} } : {}),
...(typeof input.setUpstream === 'boolean' ? { setUpstream: input.setUpstream } : {}),
...(asNonEmptyString(input.messageId) ? { messageId: input.messageId.trim() } : {}),
};
const sessionID = asNonEmptyString(input.sessionId);
const startedAt = now();
let result;
if (action === 'session.create') {
result = await sessionService.create(payload);
} else {
if (!sessionID) throw new OpenChamberControlError('sessionId is required', 400);
if (action === 'session.send') {
result = await sessionService.send(sessionID, payload);
} else {
result = await sessionService.fork(sessionID, payload);
}
}
if (input.wait !== true) {
const publicResult = { ...result };
delete publicResult.baselineAssistantMessageId;
return publicResult;
}
const client = await getClient();
const status = await waitForIdle({
client,
sessionID: result.sessionId,
directory: result.directory,
timeoutMs: normalizeWaitTimeoutMs(input.timeout),
requireActivity: result.promptDispatched === true,
baselineMessageID: result.baselineAssistantMessageId,
startedAt,
signal,
});
const publicResult = { ...result, sessionStatus: status };
delete publicResult.baselineAssistantMessageId;
if (input.lastAssistant === true) {
publicResult.lastAssistantMessage = (await sessionMessages(client, result.sessionId, result.directory, 'assistant', 1))[0] || null;
}
return publicResult;
};
const execute = async (action, input = {}, contextDirectory, options = {}) => {
try {
if (!CONTROL_ACTIONS.has(action)) {
throw new OpenChamberControlError(`Unsupported OpenChamber action: ${action || 'missing'}`, 400);
}
if (action === 'projects.list') return { projects: await projects() };
if (action === 'models.list') return models();
if (action === 'schedule.status') return scheduledTaskService.status();
if (action.startsWith('schedule.')) {
const taskID = asNonEmptyString(input.taskId);
if (SCHEDULE_TASK_ID_ACTIONS.has(action) && !taskID) {
throw new OpenChamberControlError('taskId is required', 400);
}
const explicitProjectID = asNonEmptyString(input.projectId);
const explicitDirectory = asNonEmptyString(input.directory);
const contextDirectoryFallback = explicitProjectID
? undefined
: asNonEmptyString(contextDirectory) || undefined;
const projectID = await scheduledTaskService.resolveProjectID({
projectId: explicitProjectID || undefined,
directory: explicitDirectory || contextDirectoryFallback,
});
switch (action) {
case 'schedule.list':
return { scheduler: await scheduledTaskService.status(), tasks: await scheduledTaskService.list(projectID) };
case 'schedule.create': {
const result = await scheduledTaskService.upsert(projectID, buildScheduledTask(input));
return { task: result.task, created: result.created };
}
case 'schedule.run':
return scheduledTaskService.run(projectID, taskID);
case 'schedule.delete':
return { deleted: true, tasks: await scheduledTaskService.remove(projectID, taskID) };
case 'schedule.toggle': {
if (typeof input.disabled !== 'boolean') {
throw new OpenChamberControlError('disabled is required for schedule.toggle', 400);
}
const enabled = input.disabled === false;
return { task: await scheduledTaskService.setEnabled(projectID, taskID, enabled), enabled };
}
}
}
if (action === 'session.create' || action === 'session.send' || action === 'session.fork') {
return executeSessionAction(action, input, contextDirectory, options.signal);
}
if (action.startsWith('session.')) {
const directory = asNonEmptyString(input.directory) || asNonEmptyString(contextDirectory);
const sessionID = asNonEmptyString(input.sessionId);
const client = await getClient();
if (action === 'session.list') {
const limit = positiveInteger(input.limit, 10, 'limit');
const response = await client.session.list(directory ? { directory } : {});
let sessions = Array.isArray(response?.data) ? response.data : [];
if (input.all !== true) sessions = sessions.filter((session) => !session?.time?.archived);
sessions = sessions.slice(0, limit);
if (input.withStatus === true) {
const cache = new Map();
sessions = await Promise.all(sessions.map(async (session) => {
const sessionDirectory = asNonEmptyString(session?.directory);
if (!sessionDirectory) return { ...session, status: { type: 'unknown' } };
if (!cache.has(sessionDirectory)) {
const statusRequest = client.session.status({ directory: sessionDirectory }).catch(() => null);
cache.set(sessionDirectory, statusRequest);
}
const statusResponse = await cache.get(sessionDirectory);
return { ...session, status: statusResponse?.data?.[session.id] || (statusResponse ? { type: 'idle' } : { type: 'unknown' }) };
}));
}
return { sessions, limit, directory, archived: input.all === true ? 'included' : 'excluded' };
}
if (!sessionID) throw new OpenChamberControlError('sessionId is required', 400);
if (!directory) throw new OpenChamberControlError('directory is required', 400);
if (action === 'session.status') {
return { sessionId: sessionID, directory, sessionStatus: await sessionStatus(client, sessionID, directory) };
}
if (action === 'session.messages') {
if (input.timeout !== undefined && input.wait !== true) throw new OpenChamberControlError('timeout requires wait', 400);
const role = input.lastAssistant === true ? 'assistant' : (asNonEmptyString(input.role) || 'all');
if (!['all', 'user', 'assistant'].includes(role)) throw new OpenChamberControlError('role must be all, user, or assistant', 400);
const last = input.last === true || input.lastAssistant === true;
if (input.all === true && (last || input.limit !== undefined)) throw new OpenChamberControlError('all cannot be combined with last or limit', 400);
if (last && input.limit !== undefined) throw new OpenChamberControlError('last cannot be combined with limit', 400);
const currentStatus = input.wait === true
? await waitForIdle({ client, sessionID, directory, timeoutMs: normalizeWaitTimeoutMs(input.timeout), requireActivity: false, startedAt: now(), signal: options.signal })
: await sessionStatus(client, sessionID, directory);
const limit = input.all === true ? undefined : (last ? 1 : positiveInteger(input.limit, 10, 'limit'));
return { sessionId: sessionID, directory, role, sessionStatus: currentStatus, messages: await sessionMessages(client, sessionID, directory, role, limit) };
}
}
throw new OpenChamberControlError(`Unsupported OpenChamber action: ${action || 'missing'}`, 400);
} catch (error) {
throw asControlError(error, `Failed to execute ${action || 'OpenChamber action'}`);
}
};
return { execute };
};
@@ -0,0 +1,226 @@
import { describe, expect, it, vi } from 'vitest';
import { createOpenChamberControlService } from './service.js';
const createService = (overrides = {}) => {
const client = {
session: {
list: vi.fn(async () => ({ data: [] })),
status: vi.fn(async () => ({ data: {} })),
messages: vi.fn(async () => ({ data: [] })),
},
};
const sessionService = {
create: vi.fn(async () => ({ sessionId: 'ses_1', directory: '/repo', promptDispatched: false })),
send: vi.fn(),
fork: vi.fn(),
};
const scheduledTaskService = {
status: vi.fn(async () => ({ enabledScheduledTasksCount: 0 })),
resolveProjectID: vi.fn(async () => 'project-1'),
list: vi.fn(async () => []),
upsert: vi.fn(),
run: vi.fn(),
remove: vi.fn(),
setEnabled: vi.fn(),
};
const service = createOpenChamberControlService({
readSettingsFromDiskMigrated: vi.fn(async () => ({
projects: [{ id: 'project-1', path: '/repo', label: 'Repo' }],
defaultModel: 'provider/model',
favoriteModels: [],
recentModels: [],
})),
sanitizeProjects: (projects) => projects,
buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
getOpenCodeAuthHeaders: () => ({ authorization: 'Basic test' }),
waitForOpenCodeReady: vi.fn(),
createClient: vi.fn(() => client),
sessionService,
scheduledTaskService,
...overrides,
});
return { service, client, sessionService, scheduledTaskService };
};
describe('OpenChamber control service', () => {
it('serves project and model projections without an HTTP or CLI round trip', async () => {
const { service } = createService();
await expect(service.execute('projects.list')).resolves.toEqual({
projects: [{ id: 'project-1', path: '/repo', label: 'Repo' }],
});
await expect(service.execute('models.list')).resolves.toEqual(expect.objectContaining({
defaultModel: 'provider/model',
favoriteModels: [],
}));
});
it('maps schedule creation into the shared scheduled-task service', async () => {
const { service, scheduledTaskService } = createService();
scheduledTaskService.upsert.mockResolvedValue({ task: { id: 'task-1' }, created: true });
await expect(service.execute('schedule.create', {
directory: '/repo',
name: 'Daily',
prompt: 'Run checks',
model: 'provider/model',
daily: ' 09:00 ',
goal: true,
goalTokenBudget: 5000,
})).resolves.toEqual({ task: { id: 'task-1' }, created: true });
expect(scheduledTaskService.resolveProjectID).toHaveBeenCalledWith({ projectId: undefined, directory: '/repo' });
expect(scheduledTaskService.upsert).toHaveBeenCalledWith('project-1', expect.objectContaining({
name: 'Daily',
schedule: { kind: 'daily', times: ['09:00'] },
execution: expect.objectContaining({ providerID: 'provider', modelID: 'model', goalEnabled: true, goalTokenBudget: 5000 }),
}));
});
it('does not combine an explicit schedule project with the tool context directory', async () => {
const { service, scheduledTaskService } = createService();
await service.execute('schedule.list', { projectId: ' project-1 ' }, '/current-session');
expect(scheduledTaskService.resolveProjectID).toHaveBeenCalledWith({ projectId: 'project-1', directory: undefined });
});
it('includes scheduler status alongside listed tasks', async () => {
const { service, scheduledTaskService } = createService();
scheduledTaskService.list.mockResolvedValue([{ id: 'task-1' }]);
await expect(service.execute('schedule.list', {}, '/repo')).resolves.toEqual({
scheduler: { enabledScheduledTasksCount: 0 },
tasks: [{ id: 'task-1' }],
});
});
it('toggles a scheduled task through the required disabled boolean', async () => {
const { service, scheduledTaskService } = createService();
scheduledTaskService.setEnabled.mockResolvedValue({ id: 'task-1', enabled: false });
await expect(service.execute('schedule.toggle', { taskId: 'task-1' }, '/repo')).rejects.toThrow('disabled is required for schedule.toggle');
await expect(service.execute('schedule.toggle', { taskId: 'task-1', disabled: true }, '/repo')).resolves.toEqual({
task: { id: 'task-1', enabled: false },
enabled: false,
});
expect(scheduledTaskService.setEnabled).toHaveBeenCalledWith('project-1', 'task-1', false);
});
it('returns an actionable taskId error before resolving schedule scope', async () => {
const { service, scheduledTaskService } = createService();
await expect(service.execute('schedule.run', {}, '/repo')).rejects.toThrow('taskId is required');
expect(scheduledTaskService.resolveProjectID).not.toHaveBeenCalled();
expect(scheduledTaskService.run).not.toHaveBeenCalled();
});
it('validates wait modifiers before creating a session', async () => {
const { service, sessionService } = createService();
await expect(service.execute('session.create', { directory: '/repo', timeout: 30 })).rejects.toThrow('timeout requires wait');
expect(sessionService.create).not.toHaveBeenCalled();
});
it('uses the tool context directory for session actions', async () => {
const { service, sessionService } = createService();
await service.execute('session.create', { title: 'From tool' }, '/repo');
expect(sessionService.create).toHaveBeenCalledWith({ directory: '/repo', title: 'From tool' });
});
it.each([
['session.send', 'send'],
['session.fork', 'fork'],
])('delegates %s directly to the session service', async (action, method) => {
const { service, sessionService } = createService();
sessionService[method].mockResolvedValue({ sessionId: 'ses_1', directory: '/repo' });
await service.execute(action, { sessionId: 'ses_1', directory: '/repo', prompt: 'Continue' });
expect(sessionService[method]).toHaveBeenCalledWith('ses_1', { directory: '/repo', prompt: 'Continue' });
});
it('waits past initial idle until a completed assistant result appears', async () => {
let timestamp = 1000;
const { service, client, sessionService } = createService({
now: () => timestamp,
sleep: async (duration) => { timestamp += duration; },
});
sessionService.create.mockResolvedValue({
sessionId: 'ses_1',
directory: '/repo',
promptDispatched: true,
baselineAssistantMessageId: 'msg_old',
});
client.session.status.mockResolvedValue({ data: { ses_1: { type: 'idle' } } });
client.session.messages
.mockResolvedValueOnce({ data: [{ info: { id: 'msg_old', role: 'assistant', time: { completed: 900 } }, parts: [{ type: 'text', text: 'old' }] }] })
.mockResolvedValueOnce({ data: [{ info: { id: 'msg_new', role: 'assistant', time: { completed: 1500 } }, parts: [{ type: 'text', text: 'done' }] }] })
.mockResolvedValueOnce({ data: [{ info: { id: 'msg_new', role: 'assistant', time: { completed: 1500 } }, parts: [{ type: 'text', text: 'done' }] }] });
await expect(service.execute('session.create', {
directory: '/repo',
prompt: 'work',
wait: true,
lastAssistant: true,
timeout: 2,
})).resolves.toEqual(expect.objectContaining({
sessionStatus: { type: 'idle' },
lastAssistantMessage: expect.objectContaining({ id: 'msg_new', text: 'done' }),
}));
expect(client.session.status).toHaveBeenCalledTimes(2);
});
it('filters archived sessions and adds directory-scoped statuses', async () => {
const { service, client } = createService();
client.session.list.mockResolvedValue({ data: [
{ id: 'ses_active', directory: '/repo', time: {} },
{ id: 'ses_archived', directory: '/repo', time: { archived: 100 } },
{ id: 'ses_other', directory: '/other', time: {} },
] });
client.session.status
.mockResolvedValueOnce({ data: { ses_active: { type: 'busy' } } })
.mockRejectedValueOnce(new Error('unavailable'));
await expect(service.execute('session.list', { limit: 10, withStatus: true })).resolves.toEqual({
sessions: [
{ id: 'ses_active', directory: '/repo', time: {}, status: { type: 'busy' } },
{ id: 'ses_other', directory: '/other', time: {}, status: { type: 'unknown' } },
],
limit: 10,
directory: null,
archived: 'excluded',
});
});
it('names limit in positive-integer validation errors', async () => {
const { service, client } = createService();
await expect(service.execute('session.list', { limit: 0 })).rejects.toThrow('limit must be a positive integer');
expect(client.session.list).not.toHaveBeenCalled();
});
it('projects only ordered text parts from session messages', async () => {
const { service, client } = createService();
client.session.messages.mockResolvedValue({ data: [
{
info: { id: 'msg_assistant', role: 'assistant', providerID: 'openai', modelID: 'gpt-5.4-mini', time: { created: 20, completed: 30 } },
parts: [{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'First ' }, { type: 'tool' }, { type: 'text', text: 'answer' }],
},
{ info: { id: 'msg_user', role: 'user', time: { created: 10 } }, parts: [{ type: 'text', text: 'Question' }] },
{ info: { id: 'msg_tool', role: 'assistant', time: { created: 15 } }, parts: [{ type: 'tool' }] },
] });
await expect(service.execute('session.messages', {
sessionId: 'ses_1',
directory: '/repo',
role: 'all',
all: true,
})).resolves.toEqual({
sessionId: 'ses_1',
directory: '/repo',
role: 'all',
sessionStatus: { type: 'idle' },
messages: [
{ id: 'msg_user', role: 'user', createdAt: 10, completedAt: null, model: null, text: 'Question' },
{ id: 'msg_assistant', role: 'assistant', createdAt: 20, completedAt: 30, model: 'openai/gpt-5.4-mini', text: 'First answer' },
],
});
});
it('rejects actions outside the fixed contract', async () => {
const { service } = createService();
await expect(service.execute('session.delete')).rejects.toThrow('Unsupported OpenChamber action');
});
});
@@ -0,0 +1,714 @@
import express from 'express';
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
import { createWorktree } from '../git/index.js';
import { expandSnippets } from '../opencode/snippets.js';
import { parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js';
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
import { OpenChamberControlError, asControlError } from '../openchamber-control/error.js';
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const splitModel = (value) => {
const model = asNonEmptyString(value);
if (!model) return null;
const slashIndex = model.indexOf('/');
if (slashIndex <= 0 || slashIndex === model.length - 1) return null;
return {
providerID: model.slice(0, slashIndex),
modelID: model.slice(slashIndex + 1),
};
};
const resolveRequestedModel = (payload) => {
const model = splitModel(payload?.model);
if (model) return model;
const providerID = asNonEmptyString(payload?.providerID);
const modelID = asNonEmptyString(payload?.modelID);
return providerID && modelID ? { providerID, modelID } : null;
};
const FALLBACK_PROVIDER_ID = 'opencode';
const FALLBACK_MODEL_ID = 'big-pickle';
const MIN_GOAL_TOKEN_BUDGET = 1_000;
const MAX_GOAL_TOKEN_BUDGET = 100_000_000;
const resolveGoalInput = (payload, prompt) => {
const enabled = payload?.goal === true;
if (payload?.goalTokenBudget !== undefined && !enabled) {
return { ok: false, error: 'goalTokenBudget requires goal' };
}
if (enabled && !prompt) {
return { ok: false, error: 'prompt is required when goal is enabled' };
}
if (payload?.goalTokenBudget === undefined) {
return { ok: true, enabled, tokenBudget: null };
}
const tokenBudget = payload.goalTokenBudget;
if (!Number.isSafeInteger(tokenBudget)
|| tokenBudget < MIN_GOAL_TOKEN_BUDGET
|| tokenBudget > MAX_GOAL_TOKEN_BUDGET) {
return { ok: false, error: `goalTokenBudget must be an integer from ${MIN_GOAL_TOKEN_BUDGET} to ${MAX_GOAL_TOKEN_BUDGET}` };
}
return { ok: true, enabled, tokenBudget };
};
const isPrimaryAgentMode = (mode) => !mode || mode === 'primary' || mode === 'all';
const providerModels = (provider) => {
if (Array.isArray(provider?.models)) return provider.models;
if (provider?.models && typeof provider.models === 'object') return Object.values(provider.models);
return [];
};
const hasProviderModel = (providers, providerID, modelID) => {
return providers.some((provider) => provider?.id === providerID
&& providerModels(provider).some((model) => model?.id === modelID));
};
const resolveVariant = (providers, providerID, modelID, variant) => {
const normalized = asNonEmptyString(variant);
if (!normalized) return undefined;
const provider = providers.find((entry) => entry?.id === providerID);
const model = providerModels(provider).find((entry) => entry?.id === modelID);
return model?.variants && Object.prototype.hasOwnProperty.call(model.variants, normalized)
? normalized
: undefined;
};
const parseConfigModel = (value) => splitModel(value);
const buildDirectoryHeaders = (directory) => ({
...(directory ? { 'x-opencode-directory': directory } : {}),
});
const fetchJson = async (url, authHeaders, fallback, directory) => {
const response = await fetch(url.toString(), {
headers: { ...authHeaders, ...buildDirectoryHeaders(directory), accept: 'application/json' },
});
if (!response.ok) return fallback;
return response.json().catch(() => fallback);
};
const fetchSelectionInputs = async ({ buildOpenCodeUrl, authHeaders, directory, readSettingsFromDiskMigrated }) => {
const settings = await readSettingsFromDiskMigrated();
const providersUrl = new URL(buildOpenCodeUrl('/config/providers', ''));
providersUrl.searchParams.set('directory', directory);
const agentsUrl = new URL(buildOpenCodeUrl('/agent', ''));
agentsUrl.searchParams.set('directory', directory);
const configUrl = new URL(buildOpenCodeUrl('/config', ''));
configUrl.searchParams.set('directory', directory);
const [providersBody, agentsBody, configBody] = await Promise.all([
fetchJson(providersUrl, authHeaders, { providers: [] }, directory),
fetchJson(agentsUrl, authHeaders, [], directory),
fetchJson(configUrl, authHeaders, {}, directory),
]);
return {
settings,
providers: Array.isArray(providersBody?.providers) ? providersBody.providers : [],
agents: Array.isArray(agentsBody) ? agentsBody : [],
opencodeDefaultAgent: asNonEmptyString(configBody?.default_agent) || asNonEmptyString(configBody?.defaultAgent),
opencodeDefaultModel: asNonEmptyString(configBody?.model),
};
};
const resolveDefaultSelection = ({ agents, providers, settings, opencodeDefaultAgent, opencodeDefaultModel }) => {
const primaryAgents = agents.filter((agent) => isPrimaryAgentMode(agent?.mode) && agent?.hidden !== true);
let resolvedAgent = null;
const settingsDefaultAgent = asNonEmptyString(settings?.defaultAgent);
if (settingsDefaultAgent) {
resolvedAgent = agents.find((agent) => agent?.name === settingsDefaultAgent) || null;
}
if (!resolvedAgent && opencodeDefaultAgent) {
const candidate = agents.find((agent) => agent?.name === opencodeDefaultAgent) || null;
if (candidate && isPrimaryAgentMode(candidate.mode) && candidate.hidden !== true) {
resolvedAgent = candidate;
}
}
if (!resolvedAgent) {
resolvedAgent = primaryAgents.find((agent) => agent?.name === 'build') || primaryAgents[0] || agents[0] || null;
}
let model = null;
let variant;
const settingsDefaultModel = parseConfigModel(settings?.defaultModel);
if (settingsDefaultModel && hasProviderModel(providers, settingsDefaultModel.providerID, settingsDefaultModel.modelID)) {
model = settingsDefaultModel;
variant = resolveVariant(providers, model.providerID, model.modelID, settings?.defaultVariant);
}
if (!model && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID
&& hasProviderModel(providers, resolvedAgent.model.providerID, resolvedAgent.model.modelID)) {
model = { providerID: resolvedAgent.model.providerID, modelID: resolvedAgent.model.modelID };
variant = resolveVariant(providers, model.providerID, model.modelID, resolvedAgent.variant);
}
const opencodeModel = parseConfigModel(opencodeDefaultModel);
if (!model && opencodeModel && hasProviderModel(providers, opencodeModel.providerID, opencodeModel.modelID)) {
model = opencodeModel;
}
if (!model && hasProviderModel(providers, FALLBACK_PROVIDER_ID, FALLBACK_MODEL_ID)) {
model = { providerID: FALLBACK_PROVIDER_ID, modelID: FALLBACK_MODEL_ID };
}
if (!model) {
const provider = providers[0];
const firstModel = providerModels(provider)[0];
if (provider?.id && firstModel?.id) {
model = { providerID: provider.id, modelID: firstModel.id };
}
}
return {
agent: resolvedAgent?.name,
model,
variant,
};
};
const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, directory, payload }) => {
const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`);
promptUrl.searchParams.set('directory', directory);
const response = await fetch(promptUrl.toString(), {
method: 'POST',
headers: {
...authHeaders,
...buildDirectoryHeaders(directory),
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`);
}
};
const createSession = async ({ baseUrl, authHeaders, directory, title }) => {
const sessionUrl = new URL(`${baseUrl}/session`);
sessionUrl.searchParams.set('directory', directory);
const response = await fetch(sessionUrl.toString(), {
method: 'POST',
headers: {
...authHeaders,
...buildDirectoryHeaders(directory),
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({ directory, ...(title ? { title } : {}) }),
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`session create failed (${response.status})${body ? `: ${body}` : ''}`);
}
const body = await response.json().catch(() => null);
const sessionID = body?.id || body?.data?.id;
if (!sessionID) {
throw new Error('failed to create session');
}
return sessionID;
};
const forkSession = async ({ client, sessionID, directory, messageID }) => {
const response = await client.session.fork({
sessionID,
directory,
...(messageID ? { messageID } : {}),
});
const session = response?.data;
if (!session?.id) {
throw new Error('failed to fork session');
}
return session;
};
const latestCompletedAssistantMessageID = async ({ client, sessionID, directory }) => {
let response;
try {
response = await client.session.messages({ sessionID, directory, limit: 100 });
} catch {
return null;
}
const messages = Array.isArray(response?.data) ? response.data : [];
let latest = null;
for (const message of messages) {
const info = message?.info;
if (info?.role !== 'assistant' || !Number.isFinite(info?.time?.completed)) continue;
if (!latest || (info.time.created || 0) >= (latest.time?.created || 0)) latest = info;
}
return asNonEmptyString(latest?.id);
};
const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated, sanitizeProjects, validateDirectoryPath }) => {
const projectID = asNonEmptyString(payload?.projectId) || asNonEmptyString(payload?.projectID);
if (projectID) {
const settings = await readSettingsFromDiskMigrated();
const projects = sanitizeProjects(settings?.projects || []);
const project = projects.find((entry) => entry.id === projectID) || null;
if (!project?.path) {
return { ok: false, status: 404, error: 'Project not found' };
}
const validated = await validateDirectoryPath(project.path);
return validated.ok
? { ok: true, directory: validated.directory, projectId: projectID }
: { ok: false, status: 400, error: validated.error || 'Invalid project directory' };
}
const directory = asNonEmptyString(payload?.directory);
const validated = await validateDirectoryPath(directory);
return validated.ok
? { ok: true, directory: validated.directory }
: { ok: false, status: 400, error: validated.error || 'Invalid directory' };
};
const resolveWorktreeInput = (payload) => {
if (!payload?.worktree || typeof payload.worktree !== 'object') return null;
const name = asNonEmptyString(payload.worktree.name);
if (!name) return null;
const branchName = asNonEmptyString(payload.worktree.branchName);
const startRef = asNonEmptyString(payload.worktree.startRef);
return {
mode: 'new',
name,
...(branchName ? { branchName } : {}),
...(startRef ? { startRef } : {}),
...(typeof payload.setUpstream === 'boolean' ? { setUpstream: payload.setUpstream } : {}),
};
};
export const createOpenChamberSessionService = (dependencies) => {
const {
readSettingsFromDiskMigrated,
sanitizeProjects,
validateDirectoryPath,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
emitSessionCreatedEvent,
createSessionGoal: createSessionGoalOverride,
} = dependencies;
// Last user message of an existing session, as a selection to reuse. Returns
// null when the session has no user message carrying a model.
const fetchLastUserSelection = async ({ client, sessionID, directory }) => {
try {
const response = await client.session.messages({ sessionID, directory, limit: 20 });
const records = Array.isArray(response?.data) ? response.data : [];
for (let index = records.length - 1; index >= 0; index -= 1) {
const info = records[index]?.info;
if (info?.role !== 'user') continue;
const providerID = asNonEmptyString(info.model?.providerID);
const modelID = asNonEmptyString(info.model?.modelID);
if (!providerID || !modelID) continue;
return {
model: { providerID, modelID },
agent: asNonEmptyString(info.agent),
variant: asNonEmptyString(info.model?.variant),
};
}
} catch {
}
return null;
};
const dispatchPrompt = async ({
client,
baseUrl,
authHeaders,
sessionID,
directory,
prompt,
goalInput,
requestedModel,
requestedAgent,
requestedVariant,
reuseSessionSelection = false,
}) => {
let model = requestedModel;
let agent = requestedAgent;
let variant = requestedVariant;
if (reuseSessionSelection && (!model || !agent)) {
const previous = await fetchLastUserSelection({ client, sessionID, directory });
if (previous) {
if (!model && previous.model) {
model = previous.model;
if (variant == null) variant = previous.variant ?? undefined;
}
if (!agent && previous.agent) agent = previous.agent;
}
}
if (!model || !agent) {
const inputs = await fetchSelectionInputs({
buildOpenCodeUrl,
authHeaders,
directory,
readSettingsFromDiskMigrated,
});
const defaults = resolveDefaultSelection(inputs);
if (!model) {
model = defaults.model;
if (variant == null) variant = defaults.variant;
}
agent = agent || defaults.agent;
}
if (!model) {
const error = new Error('No model is configured or available for the requested directory');
error.statusCode = 400;
throw error;
}
const expandedPrompt = expandSnippets(prompt, directory);
if (goalInput.enabled) {
await (createSessionGoalOverride || createSessionGoal)({
baseUrl,
authHeaders,
sessionID,
directory,
objective: expandedPrompt,
tokenBudget: goalInput.tokenBudget,
providerID: model.providerID,
modelID: model.modelID,
onWarning: (message, error) => console.warn(`[OpenChamberSessions] ${message}:`, error?.message || error),
});
}
const markGoalPartial = (error) => {
if (goalInput.enabled && error && typeof error === 'object') error.goalConfigured = true;
return error;
};
let dispatchedAsCommand = false;
const parsedCommand = parseScheduledCommandPrompt(prompt);
if (parsedCommand) {
let commandExists = false;
try {
const response = await client.command.list({ directory });
const commands = Array.isArray(response?.data) ? response.data : [];
commandExists = commands.some((command) => command?.name === parsedCommand.command);
} catch {
}
if (commandExists) {
try {
await client.session.command({
sessionID,
directory,
command: parsedCommand.command,
arguments: parsedCommand.arguments,
...(agent ? { agent } : {}),
model: `${model.providerID}/${model.modelID}`,
...(variant ? { variant } : {}),
});
} catch (error) {
throw markGoalPartial(error);
}
dispatchedAsCommand = true;
}
}
if (!dispatchedAsCommand) {
try {
await runPromptAsync({
baseUrl,
authHeaders,
sessionID,
directory,
payload: {
model,
...(agent ? { agent } : {}),
...(variant ? { variant } : {}),
parts: [
{ type: 'text', text: expandedPrompt },
...(goalInput.enabled
? [{ type: 'text', text: buildGoalIntroText(goalInput.tokenBudget), synthetic: true }]
: []),
],
},
});
} catch (error) {
throw markGoalPartial(error);
}
}
return { model, agent, variant, promptDispatched: true, dispatchedAsCommand };
};
const create = async (payload = {}) => {
const title = asNonEmptyString(payload.title);
const prompt = asNonEmptyString(payload.prompt);
const goalInput = resolveGoalInput(payload, prompt);
if (!goalInput.ok) {
throw new OpenChamberControlError(goalInput.error, 400);
}
const model = resolveRequestedModel(payload);
const agent = asNonEmptyString(payload.agent);
const variant = asNonEmptyString(payload.variant);
const resolvedDirectory = await resolveRequestedDirectory({
payload,
readSettingsFromDiskMigrated,
sanitizeProjects,
validateDirectoryPath,
});
if (!resolvedDirectory.ok) {
throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400);
}
const worktreeInput = resolveWorktreeInput(payload);
let worktree = null;
let sessionDirectory = resolvedDirectory.directory;
if (payload?.worktree && !worktreeInput) {
throw new OpenChamberControlError('worktree.name is required when worktree is provided', 400);
}
if (worktreeInput) {
worktree = await createWorktree(resolvedDirectory.directory, worktreeInput);
sessionDirectory = worktree.path;
}
if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
const authHeaders = getOpenCodeAuthHeaders();
const client = createOpencodeClient({ baseUrl, headers: authHeaders });
const sessionID = await createSession({
client,
baseUrl,
authHeaders,
directory: sessionDirectory,
...(title ? { title } : {}),
});
let dispatch = { model, agent, variant, promptDispatched: false, dispatchedAsCommand: false };
if (prompt) {
dispatch = await dispatchPrompt({
client,
baseUrl,
authHeaders,
sessionID,
directory: sessionDirectory,
prompt,
goalInput,
requestedModel: model,
requestedAgent: agent,
requestedVariant: variant,
});
}
const result = {
sessionId: sessionID,
directory: sessionDirectory,
...(resolvedDirectory.projectId ? { projectId: resolvedDirectory.projectId } : {}),
...(title ? { title } : {}),
...(worktree ? { worktree } : {}),
...(prompt && dispatch.model ? { model: dispatch.model } : {}),
...(prompt && dispatch.agent ? { agent: dispatch.agent } : {}),
...(prompt && dispatch.variant ? { variant: dispatch.variant } : {}),
promptDispatched: dispatch.promptDispatched,
dispatchedAsCommand: dispatch.dispatchedAsCommand,
...(goalInput.enabled ? { goalEnabled: true } : {}),
...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}),
};
try {
emitSessionCreatedEvent?.({
sessionID,
directory: sessionDirectory,
...(resolvedDirectory.projectId ? { projectID: resolvedDirectory.projectId } : {}),
...(title ? { title } : {}),
...(worktree ? { worktree } : {}),
...(prompt && dispatch.model ? { model: dispatch.model } : {}),
...(prompt && dispatch.agent ? { agent: dispatch.agent } : {}),
...(prompt && dispatch.variant ? { variant: dispatch.variant } : {}),
promptDispatched: dispatch.promptDispatched,
dispatchedAsCommand: dispatch.dispatchedAsCommand,
...(goalInput.enabled ? { goalEnabled: true } : {}),
...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}),
createdAt: Date.now(),
});
} catch {
}
return result;
};
const runExisting = async (action, sourceSessionId, payload = {}) => {
const sourceSessionID = asNonEmptyString(sourceSessionId);
const prompt = asNonEmptyString(payload.prompt);
if (!sourceSessionID) throw new OpenChamberControlError('sessionId is required', 400);
if (!prompt) throw new OpenChamberControlError('prompt is required', 400);
const goalInput = resolveGoalInput(payload, prompt);
if (!goalInput.ok) throw new OpenChamberControlError(goalInput.error, 400);
const requestedModel = resolveRequestedModel(payload);
let targetSessionID = sourceSessionID;
let targetSession = null;
let directory = null;
try {
const resolvedDirectory = await resolveRequestedDirectory({
payload,
readSettingsFromDiskMigrated,
sanitizeProjects,
validateDirectoryPath,
});
if (!resolvedDirectory.ok) {
throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400);
}
directory = resolvedDirectory.directory;
if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
const authHeaders = getOpenCodeAuthHeaders();
const client = createOpencodeClient({ baseUrl, headers: authHeaders });
if (action === 'fork') {
targetSession = await forkSession({
client,
sessionID: sourceSessionID,
directory,
messageID: asNonEmptyString(payload.messageId) || undefined,
});
targetSessionID = targetSession.id;
}
const baselineAssistantMessageId = await latestCompletedAssistantMessageID({
client,
sessionID: targetSessionID,
directory,
});
const dispatch = await dispatchPrompt({
client,
baseUrl,
authHeaders,
sessionID: targetSessionID,
directory,
prompt,
goalInput,
requestedModel,
requestedAgent: asNonEmptyString(payload.agent),
requestedVariant: asNonEmptyString(payload.variant),
reuseSessionSelection: true,
});
const result = {
action,
sessionId: targetSessionID,
directory,
...(action === 'fork' ? { sourceSessionId: sourceSessionID } : {}),
...(targetSession?.title ? { title: targetSession.title } : {}),
...(baselineAssistantMessageId ? { baselineAssistantMessageId } : {}),
model: dispatch.model,
...(dispatch.agent ? { agent: dispatch.agent } : {}),
...(dispatch.variant ? { variant: dispatch.variant } : {}),
promptDispatched: true,
dispatchedAsCommand: dispatch.dispatchedAsCommand,
...(goalInput.enabled ? { goalEnabled: true } : {}),
...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}),
};
if (action === 'fork') {
try {
emitSessionCreatedEvent?.({
sessionID: targetSessionID,
directory,
sourceSessionID,
...(targetSession?.title ? { title: targetSession.title } : {}),
model: dispatch.model,
...(dispatch.agent ? { agent: dispatch.agent } : {}),
...(dispatch.variant ? { variant: dispatch.variant } : {}),
promptDispatched: true,
dispatchedAsCommand: dispatch.dispatchedAsCommand,
...(goalInput.enabled ? { goalEnabled: true } : {}),
...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}),
createdAt: Date.now(),
});
} catch {
}
}
return result;
} catch (error) {
const statusCode = Number(error?.statusCode) || 500;
const forkCreated = action === 'fork' && targetSessionID !== sourceSessionID;
const goalConfigured = error?.goalConfigured === true;
throw new OpenChamberControlError(
error instanceof Error ? error.message : `Failed to ${action} session`,
statusCode,
{
...(forkCreated || goalConfigured
? {
partial: true,
partialAction: forkCreated ? 'fork-created' : 'goal-configured',
sessionId: targetSessionID,
directory,
}
: {}),
},
);
}
};
return {
create,
send: (sessionID, payload) => runExisting('send', sessionID, payload),
fork: (sessionID, payload) => runExisting('fork', sessionID, payload),
};
};
const sendServiceError = (res, error, fallback) => {
const controlError = asControlError(error, fallback);
return res.status(controlError.statusCode).json({
error: controlError.message,
...(controlError.partial === true ? {
partial: true,
partialAction: controlError.partialAction,
sessionId: controlError.sessionId,
directory: controlError.directory,
} : {}),
});
};
export const registerOpenChamberSessionRoutes = (app, dependencies) => {
const service = dependencies.sessionService || createOpenChamberSessionService(dependencies);
app.post('/api/openchamber/sessions', express.json({ limit: '1mb' }), async (req, res) => {
try {
return res.json(await service.create(req.body && typeof req.body === 'object' ? req.body : {}));
} catch (error) {
console.error('[OpenChamberSessions] failed to create session:', error);
return sendServiceError(res, error, 'Failed to create session');
}
});
app.post(
'/api/openchamber/sessions/:sessionId/send',
express.json({ limit: '1mb' }),
async (req, res) => {
try {
return res.json(await service.send(req.params.sessionId, req.body));
} catch (error) {
console.error('[OpenChamberSessions] failed to send session:', error);
return sendServiceError(res, error, 'Failed to send session');
}
},
);
app.post(
'/api/openchamber/sessions/:sessionId/fork',
express.json({ limit: '1mb' }),
async (req, res) => {
try {
return res.json(await service.fork(req.params.sessionId, req.body));
} catch (error) {
console.error('[OpenChamberSessions] failed to fork session:', error);
return sendServiceError(res, error, 'Failed to fork session');
}
},
);
};
@@ -0,0 +1,574 @@
import express from 'express';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
const createWorktreeMock = vi.fn(async () => ({
head: 'abc123',
name: 'side-task',
branch: 'openchamber/side-task',
path: '/repo/worktrees/side-task',
}));
const sessionCreateMock = vi.fn(async () => ({ data: { id: 'ses_123' } }));
const sessionForkMock = vi.fn(async () => ({ data: { id: 'ses_fork', title: 'Forked session' } }));
const sessionMessagesMock = vi.fn(async () => ({ data: [] }));
const sessionCommandMock = vi.fn(async () => ({ data: {} }));
const commandListMock = vi.fn(async () => ({ data: [] }));
globalThis.__openchamberCreateWorktreeMock = createWorktreeMock;
let registerOpenChamberSessionRoutes;
vi.mock('@opencode-ai/sdk/v2', () => ({
createOpencodeClient: () => ({
session: {
create: sessionCreateMock,
fork: sessionForkMock,
messages: sessionMessagesMock,
command: sessionCommandMock,
},
command: {
list: commandListMock,
},
}),
}));
vi.mock('../git/index.js', () => ({
createWorktree: (...args) => globalThis.__openchamberCreateWorktreeMock(...args),
}));
const createApp = (overrides = {}, options = {}) => {
const app = express();
if (options.globalJson !== false) {
app.use(express.json());
}
const calls = [];
registerOpenChamberSessionRoutes(app, {
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'proj_1', path: '/repo/app' }] }),
sanitizeProjects: (projects) => projects,
validateDirectoryPath: async (directory) => ({ ok: true, directory }),
buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
getOpenCodeAuthHeaders: () => ({ Authorization: 'Bearer test' }),
waitForOpenCodeReady: vi.fn(async () => undefined),
...overrides,
});
return { app, calls };
};
describe('openchamber session routes', () => {
beforeAll(async () => {
({ registerOpenChamberSessionRoutes } = await import('./routes.js'));
});
beforeEach(() => {
createWorktreeMock.mockClear();
sessionCreateMock.mockClear();
sessionForkMock.mockClear();
sessionMessagesMock.mockReset();
sessionMessagesMock.mockResolvedValue({ data: [] });
sessionCommandMock.mockReset();
sessionCommandMock.mockResolvedValue({ data: {} });
commandListMock.mockReset();
commandListMock.mockResolvedValue({ data: [] });
});
it('creates a session for a directory', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_123' }) }));
try {
const { app } = createApp();
const response = await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', title: 'Side task' })
.expect(200);
expect(response.body.sessionId).toBeTruthy();
expect(response.body.sessionId).toBe('ses_123');
expect(response.body.directory).toBe('/repo/app');
expect(response.body.promptDispatched).toBe(false);
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://opencode.test/session?directory=%2Frepo%2Fapp',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ directory: '/repo/app', title: 'Side task' }),
}),
);
expect(sessionCreateMock).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
it('parses JSON body without global middleware', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_123' }) }));
try {
const { app } = createApp({}, { globalJson: false });
const response = await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app' })
.expect(200);
expect(response.body.sessionId).toBe('ses_123');
expect(response.body.directory).toBe('/repo/app');
} finally {
globalThis.fetch = originalFetch;
}
});
it('emits a session-created event after creating a session', async () => {
const originalFetch = globalThis.fetch;
const emitSessionCreatedEvent = vi.fn();
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_123' }) }));
try {
const { app } = createApp({ emitSessionCreatedEvent });
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', title: 'Side task' })
.expect(200);
expect(emitSessionCreatedEvent).toHaveBeenCalledWith(expect.objectContaining({
sessionID: 'ses_123',
directory: '/repo/app',
title: 'Side task',
promptDispatched: false,
dispatchedAsCommand: false,
}));
} finally {
globalThis.fetch = originalFetch;
}
});
it('resolves default model and agent when prompt omits them', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (url) => {
const text = String(url);
if (text.includes('/prompt_async')) {
return { ok: true, text: async () => '' };
}
if (text.includes('/config/providers')) {
return { ok: true, json: async () => ({ providers: [{ id: 'openai', models: { 'gpt-5.5': { id: 'gpt-5.5' } } }] }) };
}
if (text.includes('/agent')) {
return { ok: true, json: async () => [{ name: 'build', mode: 'primary' }] };
}
if (text.includes('/config')) {
return { ok: true, json: async () => ({}) };
}
return { ok: true, json: async () => ({ id: 'ses_123' }) };
});
globalThis.fetch = fetchMock;
const { app } = createApp({
readSettingsFromDiskMigrated: async () => ({
defaultModel: 'openai/gpt-5.5',
defaultAgent: 'build',
projects: [{ id: 'proj_1', path: '/repo/app' }],
}),
});
try {
const response = await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', prompt: 'Run this' })
.expect(200);
expect(response.body.model).toEqual({ providerID: 'openai', modelID: 'gpt-5.5' });
expect(response.body.agent).toBe('build');
expect(fetchMock).toHaveBeenCalledWith(
'http://opencode.test/config/providers?directory=%2Frepo%2Fapp',
expect.any(Object),
);
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
expect(JSON.parse(promptCall?.[1]?.body)).toMatchObject({
model: { providerID: 'openai', modelID: 'gpt-5.5' },
agent: 'build',
});
} finally {
globalThis.fetch = originalFetch;
}
});
it('dispatches an initial prompt when model is provided', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (url) => {
if (String(url).includes('/prompt_async')) {
return { ok: true, text: async () => '' };
}
return { ok: true, json: async () => ({ id: 'ses_123' }) };
});
globalThis.fetch = fetchMock;
try {
const { app } = createApp();
const response = await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', prompt: 'Run this', model: 'openai/gpt-5.5' })
.expect(200);
expect(response.body.sessionId).toBe('ses_123');
expect(response.body.promptDispatched).toBe(true);
expect(fetchMock).toHaveBeenCalledWith(
'http://opencode.test/session/ses_123/prompt_async?directory=%2Frepo%2Fapp',
expect.objectContaining({ method: 'POST' }),
);
} finally {
globalThis.fetch = originalFetch;
}
});
it('creates goal metadata before dispatching the initial goal prompt', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (url) => {
if (String(url).includes('/prompt_async')) return { ok: true, text: async () => '' };
return { ok: true, json: async () => ({ id: 'ses_123' }) };
});
const createSessionGoal = vi.fn(async () => undefined);
globalThis.fetch = fetchMock;
try {
const { app } = createApp({ createSessionGoal });
const response = await request(app)
.post('/api/openchamber/sessions')
.send({
directory: '/repo/app',
prompt: 'Finish and verify the migration',
model: 'openai/gpt-5.5',
goal: true,
goalTokenBudget: 200000,
})
.expect(200);
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
const promptPayload = JSON.parse(promptCall[1].body);
expect(createSessionGoal).toHaveBeenCalledWith(expect.objectContaining({
sessionID: 'ses_123',
directory: '/repo/app',
objective: 'Finish and verify the migration',
tokenBudget: 200000,
providerID: 'openai',
modelID: 'gpt-5.5',
}));
expect(createSessionGoal.mock.invocationCallOrder[0]).toBeLessThan(fetchMock.mock.invocationCallOrder.at(-1));
expect(promptPayload.parts).toEqual([
{ type: 'text', text: 'Finish and verify the migration' },
expect.objectContaining({ type: 'text', synthetic: true }),
]);
expect(response.body).toMatchObject({ goalEnabled: true, goalTokenBudget: 200000, promptDispatched: true });
} finally {
globalThis.fetch = originalFetch;
}
});
it('rejects invalid goal requests before creating a session', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn();
globalThis.fetch = fetchMock;
try {
const { app } = createApp();
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', goal: true })
.expect(400, { error: 'prompt is required when goal is enabled' });
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', prompt: 'Run', goalTokenBudget: 200000 })
.expect(400, { error: 'goalTokenBudget requires goal' });
await request(app)
.post('/api/openchamber/sessions')
.send({ directory: '/repo/app', prompt: 'Run', goal: true, goalTokenBudget: 999 })
.expect(400, { error: 'goalTokenBudget must be an integer from 1000 to 100000000' });
expect(fetchMock).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
it('creates a worktree before creating a session', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async (url) => {
if (String(url).includes('/prompt_async')) {
return { ok: true, text: async () => '' };
}
return { ok: true, json: async () => ({ id: 'ses_123' }) };
});
try {
const { app } = createApp();
const response = await request(app)
.post('/api/openchamber/sessions')
.send({
directory: '/repo/app',
worktree: { name: 'side-task', branchName: 'openchamber/side-task', startRef: 'main' },
setUpstream: false,
prompt: 'Run this',
model: 'openai/gpt-5.5',
})
.expect(200);
expect(createWorktreeMock).toHaveBeenCalledWith('/repo/app', {
mode: 'new',
name: 'side-task',
branchName: 'openchamber/side-task',
startRef: 'main',
setUpstream: false,
});
expect(response.body.directory).toBe('/repo/worktrees/side-task');
expect(response.body.worktree.path).toBe('/repo/worktrees/side-task');
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://opencode.test/session/ses_123/prompt_async?directory=%2Frepo%2Fworktrees%2Fside-task',
expect.objectContaining({ method: 'POST' }),
);
} finally {
globalThis.fetch = originalFetch;
}
});
it('sends a goal prompt to an existing session after creating goal metadata', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
const createSessionGoal = vi.fn(async () => undefined);
globalThis.fetch = fetchMock;
try {
sessionMessagesMock.mockResolvedValue({
data: [{ info: { id: 'msg_before', role: 'assistant', time: { created: 10, completed: 20 } } }],
});
const { app } = createApp({ createSessionGoal });
const response = await request(app)
.post('/api/openchamber/sessions/ses_source/send')
.send({
directory: '/repo/app',
prompt: 'Apply and verify the review feedback',
model: 'openai/gpt-5.5',
agent: 'build',
variant: 'high',
goal: true,
goalTokenBudget: 200000,
})
.expect(200);
expect(response.body).toMatchObject({
action: 'send',
sessionId: 'ses_source',
directory: '/repo/app',
promptDispatched: true,
goalEnabled: true,
baselineAssistantMessageId: 'msg_before',
});
expect(createSessionGoal).toHaveBeenCalledWith(expect.objectContaining({
sessionID: 'ses_source',
directory: '/repo/app',
objective: 'Apply and verify the review feedback',
}));
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
expect(promptCall?.[0]).toBe('http://opencode.test/session/ses_source/prompt_async?directory=%2Frepo%2Fapp');
expect(createSessionGoal.mock.invocationCallOrder[0]).toBeLessThan(fetchMock.mock.invocationCallOrder.at(-1));
} finally {
globalThis.fetch = originalFetch;
}
});
it('reuses the previous session selection when send omits model, agent, and variant', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));
globalThis.fetch = fetchMock;
try {
sessionMessagesMock.mockResolvedValue({
data: [
{
info: {
id: 'msg_user',
role: 'user',
agent: 'plan',
model: { providerID: 'anthropic', modelID: 'claude-sonnet-5', variant: 'high' },
time: { created: 5 },
},
},
{ info: { id: 'msg_before', role: 'assistant', time: { created: 10, completed: 20 } } },
],
});
const { app } = createApp();
const response = await request(app)
.post('/api/openchamber/sessions/ses_source/send')
.send({ directory: '/repo/app', prompt: 'Continue where you left off' })
.expect(200);
expect(response.body).toMatchObject({
action: 'send',
sessionId: 'ses_source',
model: { providerID: 'anthropic', modelID: 'claude-sonnet-5' },
agent: 'plan',
variant: 'high',
promptDispatched: true,
});
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
const promptBody = JSON.parse(promptCall[1].body);
expect(promptBody).toMatchObject({
model: { providerID: 'anthropic', modelID: 'claude-sonnet-5' },
agent: 'plan',
variant: 'high',
});
// The default-selection inputs (config/providers/agents) must not be consulted.
expect(fetchMock.mock.calls.every(([url]) => String(url).includes('/prompt_async'))).toBe(true);
} finally {
globalThis.fetch = originalFetch;
}
});
it('forks from a message, dispatches the prompt, and emits the new session', async () => {
const originalFetch = globalThis.fetch;
const emitSessionCreatedEvent = vi.fn();
globalThis.fetch = vi.fn(async () => ({ ok: true, text: async () => '' }));
try {
const { app } = createApp({ emitSessionCreatedEvent });
const response = await request(app)
.post('/api/openchamber/sessions/ses_source/fork')
.send({
directory: '/repo/app',
messageId: 'msg_branch_point',
prompt: 'Try the alternative implementation',
model: 'openai/gpt-5.5',
agent: 'build',
variant: 'high',
})
.expect(200);
expect(sessionForkMock).toHaveBeenCalledWith({
sessionID: 'ses_source',
directory: '/repo/app',
messageID: 'msg_branch_point',
});
expect(response.body).toMatchObject({
action: 'fork',
sourceSessionId: 'ses_source',
sessionId: 'ses_fork',
directory: '/repo/app',
promptDispatched: true,
});
expect(sessionMessagesMock).toHaveBeenCalledWith({
sessionID: 'ses_fork',
directory: '/repo/app',
limit: 100,
});
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://opencode.test/session/ses_fork/prompt_async?directory=%2Frepo%2Fapp',
expect.objectContaining({ method: 'POST' }),
);
expect(emitSessionCreatedEvent).toHaveBeenCalledWith(expect.objectContaining({
sessionID: 'ses_fork',
sourceSessionID: 'ses_source',
directory: '/repo/app',
promptDispatched: true,
}));
} finally {
globalThis.fetch = originalFetch;
}
});
it('rejects send and fork requests without a prompt before calling OpenCode', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn();
globalThis.fetch = fetchMock;
try {
const { app } = createApp();
await request(app)
.post('/api/openchamber/sessions/ses_source/send')
.send({ directory: '/repo/app' })
.expect(400, { error: 'prompt is required' });
await request(app)
.post('/api/openchamber/sessions/ses_source/fork')
.send({ directory: '/repo/app' })
.expect(400, { error: 'prompt is required' });
expect(fetchMock).not.toHaveBeenCalled();
expect(sessionForkMock).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
it('reports the forked session when prompt dispatch fails', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({ ok: false, status: 500, text: async () => 'dispatch failed' }));
try {
const { app } = createApp();
const response = await request(app)
.post('/api/openchamber/sessions/ses_source/fork')
.send({
directory: '/repo/app',
prompt: 'Try another approach',
model: 'openai/gpt-5.5',
agent: 'build',
variant: 'high',
})
.expect(500);
expect(response.body).toMatchObject({
partial: true,
partialAction: 'fork-created',
sessionId: 'ses_fork',
directory: '/repo/app',
});
} finally {
globalThis.fetch = originalFetch;
}
});
it('does not apply a default variant to an explicitly requested model', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async (url) => {
const text = String(url);
if (text.includes('/prompt_async')) return { ok: true, text: async () => '' };
if (text.includes('/config/providers')) {
return {
ok: true,
json: async () => ({
providers: [
{ id: 'openai', models: { requested: { id: 'requested' }, default: { id: 'default', variants: { high: {} } } } },
],
}),
};
}
if (text.includes('/agent')) return { ok: true, json: async () => [{ name: 'build', mode: 'primary' }] };
if (text.includes('/config')) return { ok: true, json: async () => ({}) };
return { ok: true, json: async () => ({ id: 'ses_123' }) };
});
globalThis.fetch = fetchMock;
try {
const { app } = createApp({
readSettingsFromDiskMigrated: async () => ({
defaultModel: 'openai/default',
defaultVariant: 'high',
projects: [{ id: 'proj_1', path: '/repo/app' }],
}),
});
await request(app)
.post('/api/openchamber/sessions/ses_source/send')
.send({ directory: '/repo/app', prompt: 'Continue', model: 'openai/requested', agent: 'build' })
.expect(200);
const promptCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/prompt_async'));
expect(JSON.parse(promptCall[1].body)).not.toHaveProperty('variant');
} finally {
globalThis.fetch = originalFetch;
}
});
it('does not retry a failed slash command as a normal prompt', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn();
commandListMock.mockResolvedValue({ data: [{ name: 'review' }] });
sessionCommandMock.mockRejectedValue(new Error('command response failed'));
globalThis.fetch = fetchMock;
try {
const { app } = createApp();
await request(app)
.post('/api/openchamber/sessions/ses_source/send')
.send({
directory: '/repo/app',
prompt: '/review fix this',
model: 'openai/gpt-5.5',
agent: 'build',
variant: 'high',
})
.expect(500);
expect(sessionCommandMock).toHaveBeenCalledTimes(1);
expect(fetchMock).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
});
@@ -28,6 +28,7 @@ This module provides OpenCode server integration utilities for the web server ru
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics. - `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service/routes composition runtime and active-port wiring for main server startup. - `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service/routes composition runtime and active-port wiring for main server startup.
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow. - `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow.
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection, loopback authentication, and fixed CLI action dispatch.
- `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers. - `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
- `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update and models metadata route registration. - `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update and models metadata route registration.
- `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching. - `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
@@ -113,6 +114,11 @@ The runtime maintains active-session count incrementally from idempotent activit
- `waitForPortRelease(port, timeoutMs, hostname?)` - `waitForPortRelease(port, timeoutMs, hostname?)`
- `killProcessOnPort(port)` - `killProcessOnPort(port)`
Managed OpenCode launch also merges the environment returned by the agent-tool
runtime. PATH and `OPENCODE_SERVER_PASSWORD` remain lifecycle-owned and cannot
be replaced by injected values. External OpenCode processes receive no
OpenChamber tool injection.
## Public exports (env-runtime.js) ## Public exports (env-runtime.js)
- `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state. - `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state.
- OpenCode CLI resolution order is persisted settings, environment overrides, bundled Desktop CLI when available, PATH, known install locations, then platform shell discovery. - OpenCode CLI resolution order is persisted settings, environment overrides, bundled Desktop CLI when available, PATH, known install locations, then platform shell discovery.
@@ -311,6 +317,10 @@ The runtime maintains active-session count incrementally from idempotent activit
- Returned API: - Returned API:
- `run(options)` - `run(options)`
The pipeline binds the OpenChamber listener and publishes its active port
before starting managed OpenCode. The managed custom tool therefore receives
an authoritative loopback callback URL even when OpenChamber binds port `0`.
## Public exports (openchamber-routes.js) ## Public exports (openchamber-routes.js)
- `registerOpenChamberRoutes(app, dependencies)`: registers OpenChamber endpoints: - `registerOpenChamberRoutes(app, dependencies)`: registers OpenChamber endpoints:
- `GET /api/openchamber/update-check` - `GET /api/openchamber/update-check`
+4
View File
@@ -7,6 +7,7 @@ export const createBootstrapRuntime = (dependencies) => {
registerTtsRoutes, registerTtsRoutes,
registerNotificationRoutes, registerNotificationRoutes,
registerOpenChamberRoutes, registerOpenChamberRoutes,
registerAgentToolRoutes = () => {},
express, express,
} = dependencies; } = dependencies;
@@ -59,6 +60,7 @@ export const createBootstrapRuntime = (dependencies) => {
fetchFreeZenModels, fetchFreeZenModels,
getCachedZenModels, getCachedZenModels,
setAutoAcceptSession, setAutoAcceptSession,
agentToolRuntime,
} = options; } = options;
const uiAuthController = createUiAuth({ const uiAuthController = createUiAuth({
@@ -85,6 +87,8 @@ export const createBootstrapRuntime = (dependencies) => {
registerCommonRequestMiddleware(app, { express, verboseRequestLogs }); registerCommonRequestMiddleware(app, { express, verboseRequestLogs });
registerAgentToolRoutes(app, { express, agentToolRuntime });
registerAuthAndAccessRoutes(app, { registerAuthAndAccessRoutes(app, {
express, express,
tunnelAuthController, tunnelAuthController,
@@ -11,6 +11,8 @@ import { registerConfigEntityRoutes } from './config-entity-routes.js';
import { registerSettingsUtilityRoutes } from './core-routes.js'; import { registerSettingsUtilityRoutes } from './core-routes.js';
import { registerProjectIconRoutes } from './project-icon-routes.js'; import { registerProjectIconRoutes } from './project-icon-routes.js';
import { registerScheduledTaskRoutes } from '../scheduled-tasks/routes.js'; import { registerScheduledTaskRoutes } from '../scheduled-tasks/routes.js';
import { registerOpenChamberSessionRoutes } from '../openchamber-sessions/routes.js';
import { registerOpenChamberControlRoutes } from '../openchamber-control/routes.js';
import { registerSkillRoutes } from './skill-routes.js'; import { registerSkillRoutes } from './skill-routes.js';
import { registerPluginRoutes } from './plugin-routes.js'; import { registerPluginRoutes } from './plugin-routes.js';
import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js'; import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
@@ -97,8 +99,13 @@ export const createFeatureRoutesRuntime = (dependencies) => {
buildAugmentedPath, buildAugmentedPath,
projectConfigRuntime, projectConfigRuntime,
scheduledTasksRuntime, scheduledTasksRuntime,
scheduledTaskService,
openChamberSessionService,
openChamberControlService,
waitForOpenCodeReady,
getOpenChamberEventClients, getOpenChamberEventClients,
writeSseEvent, writeSseEvent,
emitSessionCreatedEvent,
permissionAutoAcceptRuntime, permissionAutoAcceptRuntime,
} = routeDependencies; } = routeDependencies;
@@ -146,10 +153,24 @@ export const createFeatureRoutesRuntime = (dependencies) => {
sanitizeProjects, sanitizeProjects,
projectConfigRuntime, projectConfigRuntime,
scheduledTasksRuntime, scheduledTasksRuntime,
scheduledTaskService,
getOpenChamberEventClients, getOpenChamberEventClients,
writeSseEvent, writeSseEvent,
}); });
registerOpenChamberSessionRoutes(app, {
readSettingsFromDiskMigrated,
sanitizeProjects,
validateDirectoryPath,
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
emitSessionCreatedEvent,
sessionService: openChamberSessionService,
});
registerOpenChamberControlRoutes(app, { controlService: openChamberControlService });
registerConfigEntityRoutes(app, { registerConfigEntityRoutes(app, {
resolveProjectDirectory, resolveProjectDirectory,
resolveOptionalProjectDirectory, resolveOptionalProjectDirectory,
@@ -38,6 +38,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
buildAugmentedPath, buildAugmentedPath,
buildManagedOpenCodePath, buildManagedOpenCodePath,
getManagedOpenCodeShellEnvSnapshot, getManagedOpenCodeShellEnvSnapshot,
getManagedOpenCodeEnv = async () => ({}),
getActiveSessionCount = () => 0, getActiveSessionCount = () => 0,
} = deps; } = deps;
@@ -474,14 +475,16 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
await applyOpencodeBinaryFromSettings({ strict: true }); await applyOpencodeBinaryFromSettings({ strict: true });
ensureOpencodeCliEnv(); ensureOpencodeCliEnv();
const openCodePassword = await ensureLocalOpenCodeServerPassword({ rotateManaged: true }); const openCodePassword = await ensureLocalOpenCodeServerPassword({ rotateManaged: true });
const envPath = typeof buildManagedOpenCodePath === 'function' let envPath = process.env.PATH;
? buildManagedOpenCodePath() if (typeof buildManagedOpenCodePath === 'function') {
: typeof buildAugmentedPath === 'function' envPath = buildManagedOpenCodePath();
? buildAugmentedPath() } else if (typeof buildAugmentedPath === 'function') {
: process.env.PATH; envPath = buildAugmentedPath();
}
const shellEnv = typeof getManagedOpenCodeShellEnvSnapshot === 'function' const shellEnv = typeof getManagedOpenCodeShellEnvSnapshot === 'function'
? getManagedOpenCodeShellEnvSnapshot() || {} ? getManagedOpenCodeShellEnvSnapshot() || {}
: {}; : {};
const managedOpenCodeEnv = await getManagedOpenCodeEnv();
try { try {
const serverInstance = await createManagedOpenCodeServerProcess({ const serverInstance = await createManagedOpenCodeServerProcess({
@@ -493,6 +496,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
env: { env: {
...shellEnv, ...shellEnv,
...process.env, ...process.env,
...managedOpenCodeEnv,
PATH: envPath, PATH: envPath,
OPENCODE_SERVER_PASSWORD: openCodePassword, OPENCODE_SERVER_PASSWORD: openCodePassword,
}, },
@@ -128,6 +128,34 @@ describe('OpenCode lifecycle', () => {
await server.close(); await server.close();
}); });
it('adds managed OpenChamber tool environment without allowing it to replace launch invariants', async () => {
const child = createMockChild();
spawnMock.mockImplementationOnce(() => {
queueMicrotask(() => {
child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
});
return child;
});
const getManagedOpenCodeEnv = vi.fn(async () => ({
OPENCODE_CONFIG_CONTENT: '{"plugin":["file:///tool.js"]}',
OPENCHAMBER_AGENT_TOOL_TOKEN: 'ephemeral',
PATH: '/untrusted/path',
OPENCODE_SERVER_PASSWORD: 'untrusted-password',
}));
const runtime = createRuntime({ getManagedOpenCodeEnv });
const server = await runtime.startOpenCode();
const [, , options] = spawnMock.mock.calls[0];
expect(getManagedOpenCodeEnv).toHaveBeenCalledOnce();
expect(options.env.OPENCODE_CONFIG_CONTENT).toBe('{"plugin":["file:///tool.js"]}');
expect(options.env.OPENCHAMBER_AGENT_TOOL_TOKEN).toBe('ephemeral');
expect(options.env.PATH).toBe('/home/user/.bun/bin:/usr/local/bin:/usr/bin');
expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
await server.close();
});
it('falls back to buildAugmentedPath when buildManagedOpenCodePath is not provided', async () => { it('falls back to buildAugmentedPath when buildManagedOpenCodePath is not provided', async () => {
delete process.env.OPENCODE_BINARY; delete process.env.OPENCODE_BINARY;
const child = createMockChild(); const child = createMockChild();
@@ -248,6 +248,12 @@ export const createSettingsHelpers = (dependencies) => {
} }
result.draftStarters = starters; result.draftStarters = starters;
} }
if (typeof candidate.draftStartersCraftGoalAdded === 'boolean') {
result.draftStartersCraftGoalAdded = candidate.draftStartersCraftGoalAdded;
}
if (typeof candidate.draftStartersScheduleTaskAdded === 'boolean') {
result.draftStartersScheduleTaskAdded = candidate.draftStartersScheduleTaskAdded;
}
if (typeof candidate.uiFont === 'string' && candidate.uiFont.length > 0) { if (typeof candidate.uiFont === 'string' && candidate.uiFont.length > 0) {
@@ -479,6 +485,9 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') { if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') {
result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications; result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications;
} }
if (typeof candidate.agentControlToolEnabled === 'boolean') {
result.agentControlToolEnabled = candidate.agentControlToolEnabled;
}
if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') { if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') {
const version = candidate.openCodeUpdateToastDismissedVersion.trim(); const version = candidate.openCodeUpdateToastDismissedVersion.trim();
result.openCodeUpdateToastDismissedVersion = version.slice(0, VERSION_STRING_MAX_LENGTH); result.openCodeUpdateToastDismissedVersion = version.slice(0, VERSION_STRING_MAX_LENGTH);
@@ -98,8 +98,6 @@ export const createStartupPipelineRuntime = (dependencies) => {
}); });
setupProxy(app); setupProxy(app);
scheduleOpenCodeApiDetection();
void bootstrapOpenCodeAtStartup();
if (apiOnly) { if (apiOnly) {
staticRoutesRuntime.registerApiOnlyFallbackRoutes(app); staticRoutesRuntime.registerApiOnlyFallbackRoutes(app);
@@ -132,6 +130,8 @@ export const createStartupPipelineRuntime = (dependencies) => {
onTunnelReady, onTunnelReady,
}); });
tunnelRuntimeContext.setActivePort(startupResult.activePort); tunnelRuntimeContext.setActivePort(startupResult.activePort);
scheduleOpenCodeApiDetection();
void bootstrapOpenCodeAtStartup();
serverStartupRuntime.attachProcessHandlers({ attachSignals }); serverStartupRuntime.attachProcessHandlers({ attachSignals });
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest';
import { createStartupPipelineRuntime } from './startup-pipeline-runtime.js';
describe('startup pipeline runtime', () => {
it('publishes the listening port before bootstrapping managed OpenCode', async () => {
const order = [];
const runtime = createStartupPipelineRuntime({
createTerminalRuntime: () => ({}),
createDictationRuntime: () => ({}),
createMessageStreamWsRuntime: () => ({}),
createServerStartupRuntime: () => ({
resolveBindHost: () => '127.0.0.1',
startListeningAndMaybeTunnel: async () => {
order.push('listen');
return { activePort: 3901 };
},
attachProcessHandlers: vi.fn(),
}),
});
await runtime.run({
app: {},
setupProxy: vi.fn(),
staticRoutesRuntime: { registerStaticRoutes: vi.fn() },
apiOnly: false,
tunnelRuntimeContext: {
setActivePort: (port) => order.push(`port:${port}`),
},
scheduleOpenCodeApiDetection: () => order.push('detect'),
bootstrapOpenCodeAtStartup: () => order.push('bootstrap'),
process: {},
crypto: {},
server: {},
attachSignals: false,
});
expect(order).toEqual(['listen', 'port:3901', 'detect', 'bootstrap']);
});
});
@@ -17,14 +17,9 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
scheduledTasksRuntime, scheduledTasksRuntime,
getOpenChamberEventClients, getOpenChamberEventClients,
writeSseEvent, writeSseEvent,
scheduledTaskService = createScheduledTaskService(dependencies),
} = dependencies; } = dependencies;
const findProjectByID = async (projectID) => {
const settings = await readSettingsFromDiskMigrated();
const projects = sanitizeProjects(settings?.projects || []);
return projects.find((project) => project.id === projectID) || null;
};
app.get('/api/projects/:projectId/scheduled-tasks', async (req, res) => { app.get('/api/projects/:projectId/scheduled-tasks', async (req, res) => {
const projectID = parseProjectID(req); const projectID = parseProjectID(req);
if (!projectID) { if (!projectID) {
@@ -32,14 +27,10 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
} }
try { try {
const project = await findProjectByID(projectID); const tasks = await scheduledTaskService.list(projectID);
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
const tasks = await projectConfigRuntime.listScheduledTasks(projectID);
return res.json({ tasks }); return res.json({ tasks });
} catch (error) { } catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
console.error('[ScheduledTasks] failed to load tasks:', error); console.error('[ScheduledTasks] failed to load tasks:', error);
return res.status(500).json({ error: 'Failed to load scheduled tasks' }); return res.status(500).json({ error: 'Failed to load scheduled tasks' });
} }
@@ -57,22 +48,9 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
} }
try { try {
const project = await findProjectByID(projectID); return res.json(await scheduledTaskService.upsert(projectID, taskInput));
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
const upserted = await projectConfigRuntime.upsertScheduledTask(projectID, taskInput);
await scheduledTasksRuntime.syncProject(projectID);
const freshTasks = await projectConfigRuntime.listScheduledTasks(projectID);
const freshTask = freshTasks.find((task) => task.id === upserted.task.id) || upserted.task;
return res.json({
tasks: freshTasks,
task: freshTask,
created: upserted.created,
});
} catch (error) { } catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
const message = error instanceof Error ? error.message : 'Failed to save scheduled task'; const message = error instanceof Error ? error.message : 'Failed to save scheduled task';
const statusCode = message.toLowerCase().includes('required') || message.toLowerCase().includes('invalid') const statusCode = message.toLowerCase().includes('required') || message.toLowerCase().includes('invalid')
? 400 ? 400
@@ -95,19 +73,9 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
} }
try { try {
const project = await findProjectByID(projectID); return res.json({ tasks: await scheduledTaskService.remove(projectID, taskID) });
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
const result = await projectConfigRuntime.deleteScheduledTask(projectID, taskID);
if (!result.deleted) {
return res.status(404).json({ error: 'Task not found' });
}
await scheduledTasksRuntime.syncProject(projectID);
const freshTasks = await projectConfigRuntime.listScheduledTasks(projectID);
return res.json({ tasks: freshTasks });
} catch (error) { } catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message });
console.error('[ScheduledTasks] failed to delete task:', error); console.error('[ScheduledTasks] failed to delete task:', error);
return res.status(500).json({ error: 'Failed to delete scheduled task' }); return res.status(500).json({ error: 'Failed to delete scheduled task' });
} }
@@ -124,31 +92,9 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
} }
try { try {
const project = await findProjectByID(projectID); return res.json({ ok: true, ...await scheduledTaskService.run(projectID, taskID) });
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
const result = await scheduledTasksRuntime.runNow(projectID, taskID);
if (result.running || result.queued) {
return res.status(409).json({ error: result.error || 'Task already running' });
}
if (result.skipped) {
return res.status(404).json({ error: 'Task not found or disabled' });
}
if (!result.ok) {
return res.status(500).json({
error: result.error || 'Task run failed',
task: result.task,
});
}
return res.json({
ok: true,
task: result.task,
sessionId: result.sessionID,
});
} catch (error) { } catch (error) {
if (error?.statusCode) return res.status(error.statusCode).json({ error: error.message, ...(error.task ? { task: error.task } : {}) });
console.error('[ScheduledTasks] failed to run task:', error); console.error('[ScheduledTasks] failed to run task:', error);
return res.status(500).json({ error: 'Failed to run scheduled task' }); return res.status(500).json({ error: 'Failed to run scheduled task' });
} }
@@ -156,37 +102,7 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
app.get('/api/openchamber/scheduled-tasks/status', async (_req, res) => { app.get('/api/openchamber/scheduled-tasks/status', async (_req, res) => {
try { try {
if (typeof scheduledTasksRuntime.getStatus === 'function') { return res.json(await scheduledTaskService.status());
return res.json(scheduledTasksRuntime.getStatus());
}
const settings = await readSettingsFromDiskMigrated();
const projects = sanitizeProjects(settings?.projects || []);
let enabledCount = 0;
let runningCount = 0;
for (const project of projects) {
try {
const tasks = await projectConfigRuntime.listScheduledTasks(project.id);
for (const task of tasks) {
if (task?.enabled) {
enabledCount += 1;
}
if (task?.state?.lastStatus === 'running') {
runningCount += 1;
}
}
} catch {
}
}
return res.json({
hasEnabledScheduledTasks: enabledCount > 0,
hasRunningScheduledTasks: runningCount > 0,
enabledScheduledTasksCount: enabledCount,
runningScheduledTasksCount: runningCount,
});
} catch (error) { } catch (error) {
console.error('[ScheduledTasks] failed to resolve scheduled task status:', error); console.error('[ScheduledTasks] failed to resolve scheduled task status:', error);
return res.status(500).json({ error: 'Failed to resolve scheduled task status' }); return res.status(500).json({ error: 'Failed to resolve scheduled task status' });
@@ -233,3 +149,4 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
}); });
}); });
}; };
import { createScheduledTaskService } from './service.js';
@@ -2,6 +2,7 @@ import { createOpencodeClient } from '@opencode-ai/sdk/v2';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import parser from 'cron-parser'; import parser from 'cron-parser';
import { expandSnippets } from '../opencode/snippets.js'; import { expandSnippets } from '../opencode/snippets.js';
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
const DEFAULT_GLOBAL_CONCURRENCY = 4; const DEFAULT_GLOBAL_CONCURRENCY = 4;
const DEFAULT_PROJECT_CONCURRENCY = 2; const DEFAULT_PROJECT_CONCURRENCY = 2;
@@ -407,21 +408,6 @@ export const createScheduledTasksRuntime = (deps) => {
return projectRunning < maxProjectConcurrency; return projectRunning < maxProjectConcurrency;
}; };
// Same instruction the composer attaches on an armed goal send: the agent
// must know goal mode is on from turn one, and each turn has to end with a
// factual report for the independent audit.
const buildGoalIntroText = (tokenBudget) => {
const budgetLine = tokenBudget
? ` A token budget of ${tokenBudget} tokens applies to this goal.`
: '';
return '<system-reminder>\n'
+ 'Goal mode is active for this session. The user message above defines the goal objective. '
+ 'Work toward it across turns; whenever you stop before the objective is verifiably complete, the system will automatically prompt you to continue. '
+ 'Progress is evaluated independently after each turn, so end every turn with a clear, factual statement of what is done, what was verified, and what remains.'
+ budgetLine
+ '\n</system-reminder>';
};
const buildPromptAsyncPayload = (task, projectPath) => ({ const buildPromptAsyncPayload = (task, projectPath) => ({
model: { model: {
providerID: task.execution.providerID, providerID: task.execution.providerID,
@@ -440,88 +426,6 @@ export const createScheduledTasksRuntime = (deps) => {
], ],
}); });
// Scheduled goal runs: stamp the goal onto the fresh session's metadata
// before the prompt goes out; the session-goal runtime picks the loop up
// from session events like any other goal.
const createTaskGoal = async ({ baseUrl, authHeaders, sessionID, projectPath, task }) => {
const now = Date.now();
// File-backed objective keyed by session id: metadata stays light, the
// full expanded prompt lives under the OpenChamber data dir. If the file
// write fails, fall back to an inline (clamped) objective.
// Oversized prompts are distilled into audit criteria by the small model
// (the working agent gets the full prompt in chat anyway); on distill
// failure a head+tail excerpt keeps intent and acceptance criteria.
let objectiveText = expandSnippets(task.execution.prompt, projectPath);
if (objectiveText.length > 5000) {
let distilled = null;
try {
const { generateSmallModelText } = await import('../small-model/index.js');
const generated = await generateSmallModelText({
restrictToPreferredProvider: true,
prompt: objectiveText,
system: [
'You distill a large task description into the COMPLETION CRITERIA a progress auditor will judge against.',
'Return ONLY the criteria text — no preamble, no headers, no markdown fences.',
'Capture: the end goals, what must exist and work when the task is fully done, and how each major part is verified. Omit implementation steps.',
'Preserve verbatim any file paths, commands, and identifiers that define the task.',
'Stay under 4000 characters.',
'Write in the same language as the task text.',
].join('\n'),
directory: projectPath,
preferredProviderID: task.execution.providerID,
preferredModelID: task.execution.modelID,
});
distilled = typeof generated?.text === 'string' ? generated.text.trim() : null;
} catch (error) {
console.warn('[scheduled-tasks] goal objective distillation failed:', error?.message || error);
}
if (distilled) {
objectiveText = distilled;
} else {
const marker = '\n\n[… objective trimmed for the auditor — the full prompt was delivered in the chat message …]\n\n';
const half = Math.max(0, Math.floor((5000 - marker.length) / 2));
objectiveText = `${objectiveText.slice(0, half)}${marker}${objectiveText.slice(-half)}`;
}
}
let objectiveFile = false;
try {
const { writeObjective } = await import('../session-goal/objectives.js');
await writeObjective(sessionID, objectiveText);
objectiveFile = true;
} catch (error) {
console.warn('[scheduled-tasks] goal objective file write failed, falling back to inline:', error?.message || error);
}
const goal = {
id: `${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`,
objective: objectiveFile ? '' : objectiveText.slice(0, 5000),
objectiveFile,
status: 'active',
tokenBudget: task.execution.goalTokenBudget || null,
tokensUsed: 0,
turnsUsed: 0,
blockedStreak: 0,
note: '',
statusReason: '',
lastAccountedMessageID: '',
createdAt: now,
updatedAt: now,
};
const url = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
url.searchParams.set('directory', projectPath);
const response = await fetch(url.toString(), {
method: 'PATCH',
headers: {
...authHeaders,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({ metadata: { openchamber: { goal } } }),
});
if (!response.ok) {
throw new Error(`goal metadata patch failed (${response.status})`);
}
};
const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, projectPath, task }) => { const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, projectPath, task }) => {
const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`); const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`);
promptUrl.searchParams.set('directory', projectPath); promptUrl.searchParams.set('directory', projectPath);
@@ -624,7 +528,17 @@ export const createScheduledTasksRuntime = (deps) => {
} }
if (task.execution.goalEnabled) { if (task.execution.goalEnabled) {
await createTaskGoal({ baseUrl, authHeaders, sessionID, projectPath, task }); await createSessionGoal({
baseUrl,
authHeaders,
sessionID,
directory: projectPath,
objective: expandSnippets(task.execution.prompt, projectPath),
tokenBudget: task.execution.goalTokenBudget,
providerID: task.execution.providerID,
modelID: task.execution.modelID,
onWarning: (message, error) => console.warn(`[scheduled-tasks] ${message}:`, error?.message || error),
});
} }
const executedAsCommand = await runScheduledCommandIfApplicable({ const executedAsCommand = await runScheduledCommandIfApplicable({
@@ -0,0 +1,145 @@
import path from 'node:path';
import { OpenChamberControlError } from '../openchamber-control/error.js';
const asNonEmptyString = (value) => {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
export const createScheduledTaskService = (dependencies) => {
const {
readSettingsFromDiskMigrated,
sanitizeProjects,
projectConfigRuntime,
scheduledTasksRuntime,
} = dependencies;
const listProjects = async () => {
const settings = await readSettingsFromDiskMigrated();
return sanitizeProjects(settings?.projects || []);
};
const findProjectByID = async (projectID) => {
const normalized = asNonEmptyString(projectID);
if (!normalized) throw new OpenChamberControlError('projectId is required', 400);
const projects = await listProjects();
const project = projects.find((entry) => entry.id === normalized) || null;
if (!project) throw new OpenChamberControlError('Project not found', 404);
return project;
};
const resolveProjectID = async ({ projectId, directory } = {}) => {
const requestedProjectID = asNonEmptyString(projectId);
const requestedDirectory = asNonEmptyString(directory);
if (requestedProjectID && requestedDirectory) {
throw new OpenChamberControlError('Provide only one of projectId or directory', 400);
}
if (requestedProjectID) {
await findProjectByID(requestedProjectID);
return requestedProjectID;
}
if (!requestedDirectory) throw new OpenChamberControlError('projectId or directory is required', 400);
const resolvedDirectory = path.resolve(requestedDirectory);
const projects = await listProjects();
const project = projects.find((entry) => path.resolve(entry.path) === resolvedDirectory);
if (!project) throw new OpenChamberControlError(`Project not found for directory: ${resolvedDirectory}`, 404);
return project.id;
};
const list = async (projectID) => {
await findProjectByID(projectID);
return projectConfigRuntime.listScheduledTasks(projectID);
};
const upsert = async (projectID, taskInput) => {
await findProjectByID(projectID);
if (!taskInput || typeof taskInput !== 'object') {
throw new OpenChamberControlError('task payload is required', 400);
}
let upserted;
try {
upserted = await projectConfigRuntime.upsertScheduledTask(projectID, taskInput);
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to save scheduled task';
const invalid = message.toLowerCase().includes('required') || message.toLowerCase().includes('invalid');
throw new OpenChamberControlError(message, invalid ? 400 : 500);
}
await scheduledTasksRuntime.syncProject(projectID);
const tasks = await projectConfigRuntime.listScheduledTasks(projectID);
return {
tasks,
task: tasks.find((task) => task.id === upserted.task.id) || upserted.task,
created: upserted.created,
};
};
const remove = async (projectID, taskID) => {
await findProjectByID(projectID);
const normalizedTaskID = asNonEmptyString(taskID);
if (!normalizedTaskID) throw new OpenChamberControlError('taskId is required', 400);
const result = await projectConfigRuntime.deleteScheduledTask(projectID, normalizedTaskID);
if (!result.deleted) throw new OpenChamberControlError('Task not found', 404);
await scheduledTasksRuntime.syncProject(projectID);
return projectConfigRuntime.listScheduledTasks(projectID);
};
const run = async (projectID, taskID) => {
await findProjectByID(projectID);
const normalizedTaskID = asNonEmptyString(taskID);
if (!normalizedTaskID) throw new OpenChamberControlError('taskId is required', 400);
const result = await scheduledTasksRuntime.runNow(projectID, normalizedTaskID);
if (result.running || result.queued) {
throw new OpenChamberControlError(result.error || 'Task already running', 409);
}
if (result.skipped) throw new OpenChamberControlError('Task not found or disabled', 404);
if (!result.ok) {
throw new OpenChamberControlError(result.error || 'Task run failed', 500, { task: result.task });
}
return { task: result.task, sessionId: result.sessionID };
};
const setEnabled = async (projectID, taskID, enabled) => {
const tasks = await list(projectID);
const task = tasks.find((entry) => entry?.id === taskID);
if (!task) throw new OpenChamberControlError('Task not found', 404);
const result = await upsert(projectID, { ...task, enabled });
return result.task;
};
const status = async () => {
if (typeof scheduledTasksRuntime.getStatus === 'function') {
return scheduledTasksRuntime.getStatus();
}
const projects = await listProjects();
let enabledCount = 0;
let runningCount = 0;
for (const project of projects) {
try {
const tasks = await projectConfigRuntime.listScheduledTasks(project.id);
for (const task of tasks) {
if (task?.enabled) enabledCount += 1;
if (task?.state?.lastStatus === 'running') runningCount += 1;
}
} catch {
}
}
return {
hasEnabledScheduledTasks: enabledCount > 0,
hasRunningScheduledTasks: runningCount > 0,
enabledScheduledTasksCount: enabledCount,
runningScheduledTasksCount: runningCount,
};
};
return {
listProjects,
resolveProjectID,
list,
upsert,
remove,
run,
setEnabled,
status,
};
};
@@ -66,8 +66,9 @@ before touching the filesystem). Rationale: metadata rides every
- UI display fetches content via the GET route - UI display fetches content via the GET route
(`useGoalObjectiveContent`); in VS Code the route is unavailable, so the (`useGoalObjectiveContent`); in VS Code the route is unavailable, so the
strip degrades to the audit note (display-only fallback by design). strip degrades to the audit note (display-only fallback by design).
- Scheduled goal tasks write the file server-side directly via - Server-created goals write the file through `create.js`, which also owns
`objectives.js`. objective fitting, inline fallback, metadata creation, and the synthetic
first-turn reminder shared by scheduled tasks and CLI-created sessions.
## Flow ## Flow
@@ -174,6 +175,24 @@ stamp `metadata.openchamber.goal` onto the fresh session (objective = the
expanded task prompt) and attach the goal-mode intro part to the prompt. expanded task prompt) and attach the goal-mode intro part to the prompt.
The loop here picks it up from session events like any other goal. The loop here picks it up from session events like any other goal.
## CLI-created goals
`openchamber session create --prompt <text> --goal` uses the explicit
`POST /api/openchamber/sessions` orchestration route. The server creates the
session, fits and stores the expanded prompt as its objective, patches active
goal metadata, appends the synthetic goal reminder, and only then dispatches
the prompt. `--goal-token-budget` applies the same optional budget contract as
scheduled goals. Slash commands retain command dispatch semantics and cannot
carry the synthetic prompt part; the goal metadata is still installed before
the command runs.
`openchamber session send --goal` and `openchamber session fork --goal` use
the same server-owned prompt orchestration. Send installs a fresh goal on the
target session; fork first uses the official OpenCode fork operation (at the
optional message boundary), then installs the goal on the new session. Both
preserve the objective-file-before-metadata and metadata-before-dispatch
ordering used by create and scheduled goals.
## Limitations ## Limitations
- Web-server feature: VS Code (extension-only) renders goal state via - Web-server feature: VS Code (extension-only) renders goal state via
@@ -0,0 +1,112 @@
import { GOAL_OBJECTIVE_CHAR_LIMIT, writeObjective } from './objectives.js';
const TRIM_MARKER = '\n\n[… objective trimmed for the auditor — the full prompt was delivered in the chat message …]\n\n';
export const buildGoalIntroText = (tokenBudget) => {
const budgetLine = tokenBudget
? ` A token budget of ${tokenBudget} tokens applies to this goal.`
: '';
return '<system-reminder>\n'
+ 'Goal mode is active for this session. The user message above defines the goal objective. '
+ 'Work toward it across turns; whenever you stop before the objective is verifiably complete, the system will automatically prompt you to continue. '
+ 'Progress is evaluated independently after each turn, so end every turn with a clear, factual statement of what is done, what was verified, and what remains.'
+ budgetLine
+ '\n</system-reminder>';
};
const fitObjective = async ({ objective, directory, providerID, modelID, warn }) => {
if (objective.length <= GOAL_OBJECTIVE_CHAR_LIMIT) return objective;
let distilled = null;
try {
const { generateSmallModelText } = await import('../small-model/index.js');
const generated = await generateSmallModelText({
restrictToPreferredProvider: true,
prompt: objective,
system: [
'You distill a large task description into the COMPLETION CRITERIA a progress auditor will judge against.',
'Return ONLY the criteria text — no preamble, no headers, no markdown fences.',
'Capture: the end goals, what must exist and work when the task is fully done, and how each major part is verified. Omit implementation steps.',
'Preserve verbatim any file paths, commands, and identifiers that define the task.',
'Stay under 4000 characters.',
'Write in the same language as the task text.',
].join('\n'),
directory,
preferredProviderID: providerID,
preferredModelID: modelID,
});
distilled = typeof generated?.text === 'string' ? generated.text.trim() : null;
} catch (error) {
warn('goal objective distillation failed', error);
}
if (distilled) return distilled.slice(0, GOAL_OBJECTIVE_CHAR_LIMIT);
const half = Math.max(0, Math.floor((GOAL_OBJECTIVE_CHAR_LIMIT - TRIM_MARKER.length) / 2));
return `${objective.slice(0, half)}${TRIM_MARKER}${objective.slice(-half)}`;
};
export const createSessionGoal = async ({
baseUrl,
authHeaders,
sessionID,
directory,
objective,
tokenBudget = null,
providerID,
modelID,
onWarning,
}) => {
const warn = (message, error) => {
if (typeof onWarning === 'function') {
onWarning(message, error);
return;
}
console.warn(`[session-goal] ${message}:`, error?.message || error);
};
const objectiveText = await fitObjective({
objective: String(objective ?? '').trim(),
directory,
providerID,
modelID,
warn,
});
if (!objectiveText) throw new Error('goal objective is required');
let objectiveFile = false;
try {
await writeObjective(sessionID, objectiveText);
objectiveFile = true;
} catch (error) {
warn('goal objective file write failed, falling back to inline', error);
}
const now = Date.now();
const goal = {
id: `${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`,
objective: objectiveFile ? '' : objectiveText.slice(0, GOAL_OBJECTIVE_CHAR_LIMIT),
objectiveFile,
status: 'active',
tokenBudget: tokenBudget || null,
tokensUsed: 0,
turnsUsed: 0,
blockedStreak: 0,
note: '',
statusReason: '',
lastAccountedMessageID: '',
createdAt: now,
updatedAt: now,
};
const url = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
url.searchParams.set('directory', directory);
const response = await fetch(url.toString(), {
method: 'PATCH',
headers: {
...authHeaders,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({ metadata: { openchamber: { goal } } }),
});
if (!response.ok) throw new Error(`goal metadata patch failed (${response.status})`);
return goal;
};
@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const writeObjectiveMock = vi.fn(async () => undefined);
const generateSmallModelTextMock = vi.fn(async () => ({ text: '' }));
vi.mock('./objectives.js', () => ({
GOAL_OBJECTIVE_CHAR_LIMIT: 5_000,
writeObjective: writeObjectiveMock,
}));
vi.mock('../small-model/index.js', () => ({
generateSmallModelText: generateSmallModelTextMock,
}));
const { buildGoalIntroText, createSessionGoal } = await import('./create.js');
describe('session goal creation', () => {
beforeEach(() => {
writeObjectiveMock.mockReset().mockResolvedValue(undefined);
generateSmallModelTextMock.mockReset().mockResolvedValue({ text: '' });
});
it('writes the objective before patching active goal metadata', async () => {
const fetchMock = vi.fn(async () => ({ ok: true }));
const originalFetch = globalThis.fetch;
globalThis.fetch = fetchMock;
try {
const goal = await createSessionGoal({
baseUrl: 'http://opencode.test',
authHeaders: { Authorization: 'Bearer test' },
sessionID: 'ses_123',
directory: '/repo/app',
objective: 'Finish and verify the migration',
tokenBudget: 200_000,
providerID: 'openai',
modelID: 'gpt-5.5',
});
expect(writeObjectiveMock).toHaveBeenCalledWith('ses_123', 'Finish and verify the migration');
expect(writeObjectiveMock.mock.invocationCallOrder[0]).toBeLessThan(fetchMock.mock.invocationCallOrder[0]);
expect(goal).toMatchObject({ objective: '', objectiveFile: true, status: 'active', tokenBudget: 200_000 });
expect(fetchMock).toHaveBeenCalledWith(
'http://opencode.test/session/ses_123?directory=%2Frepo%2Fapp',
expect.objectContaining({ method: 'PATCH' }),
);
} finally {
globalThis.fetch = originalFetch;
}
});
it('falls back to inline metadata when objective storage fails', async () => {
writeObjectiveMock.mockRejectedValueOnce(new Error('disk unavailable'));
const fetchMock = vi.fn(async () => ({ ok: true }));
const originalFetch = globalThis.fetch;
globalThis.fetch = fetchMock;
try {
await createSessionGoal({
baseUrl: 'http://opencode.test',
authHeaders: {},
sessionID: 'ses_123',
directory: '/repo/app',
objective: 'Finish the migration',
onWarning: vi.fn(),
});
const payload = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(payload.metadata.openchamber.goal).toMatchObject({
objective: 'Finish the migration',
objectiveFile: false,
});
} finally {
globalThis.fetch = originalFetch;
}
});
it('builds the same goal intro with an optional budget', () => {
expect(buildGoalIntroText(null)).toContain('Goal mode is active for this session.');
expect(buildGoalIntroText(200_000)).toContain('A token budget of 200000 tokens applies to this goal.');
});
});
+24 -4
View File
@@ -17,6 +17,13 @@ const repoRoot = resolve(__dirname, "..")
const remixPath = resolve(repoRoot, "node_modules/@remixicon/react/index.mjs") const remixPath = resolve(repoRoot, "node_modules/@remixicon/react/index.mjs")
const outPath = resolve(repoRoot, "packages/ui/src/components/icon/sprite.ts") const outPath = resolve(repoRoot, "packages/ui/src/components/icon/sprite.ts")
const customIconData = new Map([
[
"openchamber",
`<polygon points="12 2.5 3.5 7.4 3.5 17.2 12 22.1 20.5 17.2 20.5 7.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><polyline points="3.5 7.4 12 12.3 20.5 7.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="12.3" x2="12" y2="22.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="m12 5.5 3.7 2.1L12 9.7 8.3 7.6 12 5.5Zm0 1.5-1 .6 1 .6 1-.6-1-.6Z" fill="currentColor" fill-rule="evenodd"/>`,
],
])
const source = readFileSync(remixPath, "utf-8") const source = readFileSync(remixPath, "utf-8")
// --- Step 1: extract variable → path mapping --- // --- Step 1: extract variable → path mapping ---
@@ -142,7 +149,13 @@ function findAllSourceFiles(dir) {
const allSrcFiles = findAllSourceFiles(srcDir) const allSrcFiles = findAllSourceFiles(srcDir)
const usedIcons = new Set() const usedIcons = new Set()
const usedCustomIcons = new Set()
const addKebabIcon = (kebab) => { const addKebabIcon = (kebab) => {
if (customIconData.has(kebab)) {
usedCustomIcons.add(kebab)
return true
}
const exactRiName = spriteNameToRi.get(kebab) const exactRiName = spriteNameToRi.get(kebab)
if (exactRiName && !hasRemixVariantSuffix(exactRiName)) { if (exactRiName && !hasRemixVariantSuffix(exactRiName)) {
usedIcons.add(exactRiName) usedIcons.add(exactRiName)
@@ -352,11 +365,18 @@ for (const iconName of [...usedIcons].sort()) {
iconEntries.push({ name: iconName, content: svgContent }) iconEntries.push({ name: iconName, content: svgContent })
} }
for (const iconName of [...usedCustomIcons].sort()) {
iconEntries.push({ name: iconName, content: customIconData.get(iconName) })
}
// --- Step 5: write sprite.ts --- // --- Step 5: write sprite.ts ---
const spriteLines = iconEntries.map(({ name, content }) => { const spriteLines = iconEntries
const spriteName = remixToSpriteName(name) .map(({ name, content }) => ({
return ` "${spriteName}": \`${content}\`,` name: name.startsWith("Ri") ? remixToSpriteName(name) : name,
}) content,
}))
.sort((left, right) => left.name.localeCompare(right.name))
.map(({ name, content }) => ` "${name}": \`${content}\`,`)
const spriteContent = `// This file is auto-generated by scripts/generate-icon-sprite.mjs const spriteContent = `// This file is auto-generated by scripts/generate-icon-sprite.mjs
// Do not edit manually. Run the script to update. // Do not edit manually. Run the script to update.