one-code-extension 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -202
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row models for the /plugins panel tabs (pure).
|
|
3
|
+
*
|
|
4
|
+
* The Installed tab's grouping is One Code's own heuristic (Claude Code's
|
|
5
|
+
* usage-recency data doesn't exist here): Favorites float first, then
|
|
6
|
+
* "Needs attention" (MCP servers that failed or need auth), then plugins split
|
|
7
|
+
* into "Not used recently" (no recorded command/skill use in 30 days,
|
|
8
|
+
* including never) and "Plugins", then healthy "MCP servers", then "Skills".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { McpServerStatus } from "../../lib/mcp-status.ts";
|
|
12
|
+
import type { Plugin } from "../../lib/plugins.ts";
|
|
13
|
+
import type { ScannedSkill } from "../../lib/skill-scan.ts";
|
|
14
|
+
import { isSkillEnabled, type SkillState, skillOverrideKey } from "../../lib/skill-overrides.ts";
|
|
15
|
+
import { formatRecency, type UsageEntry, usageKey } from "../../lib/usage-tracker.ts";
|
|
16
|
+
import type { Favorites } from "../../lib/favorites.ts";
|
|
17
|
+
import type { MarketplaceSnapshot } from "../marketplace/sync.ts";
|
|
18
|
+
import type { KnownMarketplace } from "../marketplace/types.ts";
|
|
19
|
+
import { OFFICIAL_MARKETPLACE_NAME } from "../marketplace/official.ts";
|
|
20
|
+
|
|
21
|
+
const RECENT_MS = 30 * 24 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
export interface DiscoverRow {
|
|
24
|
+
/** `name@marketplace`. */
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
marketplace: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
installs?: number;
|
|
30
|
+
installed: boolean;
|
|
31
|
+
busy: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildDiscoverRows(input: {
|
|
35
|
+
snapshots: MarketplaceSnapshot[];
|
|
36
|
+
installedIds: Set<string>;
|
|
37
|
+
counts?: Map<string, number>;
|
|
38
|
+
busy: Set<string>;
|
|
39
|
+
search: string;
|
|
40
|
+
}): DiscoverRow[] {
|
|
41
|
+
const query = input.search.trim().toLowerCase();
|
|
42
|
+
const rows: DiscoverRow[] = [];
|
|
43
|
+
for (const snapshot of input.snapshots) {
|
|
44
|
+
if (!snapshot.manifest) continue;
|
|
45
|
+
for (const entry of snapshot.manifest.plugins) {
|
|
46
|
+
const id = `${entry.name}@${snapshot.name}`;
|
|
47
|
+
if (query && !entry.name.toLowerCase().includes(query) && !(entry.description ?? "").toLowerCase().includes(query)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
rows.push({
|
|
51
|
+
id,
|
|
52
|
+
name: entry.name,
|
|
53
|
+
marketplace: snapshot.name,
|
|
54
|
+
description: entry.description,
|
|
55
|
+
installs: input.counts?.get(id),
|
|
56
|
+
installed: input.installedIds.has(id),
|
|
57
|
+
busy: input.busy.has(id),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
rows.sort((a, b) => (b.installs ?? -1) - (a.installs ?? -1) || a.name.localeCompare(b.name));
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type InstalledRow =
|
|
66
|
+
| { kind: "section"; title: string }
|
|
67
|
+
| {
|
|
68
|
+
kind: "plugin";
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
marketplace?: string;
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
origin: Plugin["originRoot"];
|
|
74
|
+
overridden: boolean;
|
|
75
|
+
favorite: boolean;
|
|
76
|
+
busy: boolean;
|
|
77
|
+
}
|
|
78
|
+
| { kind: "mcp"; name: string; status: McpServerStatus["status"]; detail?: string; toolCount?: number; source?: string }
|
|
79
|
+
| {
|
|
80
|
+
kind: "skill";
|
|
81
|
+
overrideKey: string;
|
|
82
|
+
name: string;
|
|
83
|
+
scope: ScannedSkill["scope"];
|
|
84
|
+
tokens: number;
|
|
85
|
+
recency: string;
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
favorite: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Latest use across a plugin's namespaced commands/skills (`<plugin>:<name>` keys). */
|
|
91
|
+
function pluginLastUsed(plugin: Plugin, usage: Record<string, UsageEntry>): number | undefined {
|
|
92
|
+
let latest: number | undefined;
|
|
93
|
+
for (const [key, entry] of Object.entries(usage)) {
|
|
94
|
+
const [, name] = key.split(/:(.+)/);
|
|
95
|
+
if (!name?.startsWith(`${plugin.name}:`)) continue;
|
|
96
|
+
const at = Date.parse(entry.lastUsedAt);
|
|
97
|
+
if (!Number.isNaN(at) && (latest === undefined || at > latest)) latest = at;
|
|
98
|
+
}
|
|
99
|
+
return latest;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function buildInstalledRows(input: {
|
|
103
|
+
plugins: Plugin[];
|
|
104
|
+
mcpServers?: McpServerStatus[];
|
|
105
|
+
skills: Array<ScannedSkill & { tokens: number }>;
|
|
106
|
+
usage: Record<string, UsageEntry>;
|
|
107
|
+
skillOverrides: Record<string, SkillState>;
|
|
108
|
+
favorites: Favorites;
|
|
109
|
+
busy: Set<string>;
|
|
110
|
+
search: string;
|
|
111
|
+
now?: Date;
|
|
112
|
+
}): InstalledRow[] {
|
|
113
|
+
const query = input.search.trim().toLowerCase();
|
|
114
|
+
const matches = (name: string) => !query || name.toLowerCase().includes(query);
|
|
115
|
+
const now = (input.now ?? new Date()).getTime();
|
|
116
|
+
|
|
117
|
+
const pluginRow = (plugin: Plugin): Extract<InstalledRow, { kind: "plugin" }> => ({
|
|
118
|
+
kind: "plugin",
|
|
119
|
+
id: plugin.id,
|
|
120
|
+
name: plugin.name,
|
|
121
|
+
marketplace: plugin.marketplace,
|
|
122
|
+
enabled: plugin.enabled,
|
|
123
|
+
origin: plugin.originRoot,
|
|
124
|
+
overridden: plugin.overridden ?? false,
|
|
125
|
+
favorite: input.favorites.plugins.includes(plugin.id),
|
|
126
|
+
busy: input.busy.has(plugin.id),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const skillRow = (skill: ScannedSkill & { tokens: number }): Extract<InstalledRow, { kind: "skill" }> => {
|
|
130
|
+
const key = skillOverrideKey(skill.scope, skill.name);
|
|
131
|
+
return {
|
|
132
|
+
kind: "skill",
|
|
133
|
+
overrideKey: key,
|
|
134
|
+
name: skill.name,
|
|
135
|
+
scope: skill.scope,
|
|
136
|
+
tokens: skill.tokens,
|
|
137
|
+
recency: formatRecency(input.usage[usageKey("skill", skill.name)], input.now),
|
|
138
|
+
enabled: isSkillEnabled(input.skillOverrides, key),
|
|
139
|
+
favorite: input.favorites.skills.includes(key),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const mcpRow = (server: McpServerStatus): Extract<InstalledRow, { kind: "mcp" }> => ({
|
|
144
|
+
kind: "mcp",
|
|
145
|
+
name: server.name,
|
|
146
|
+
status: server.status,
|
|
147
|
+
detail: server.detail,
|
|
148
|
+
toolCount: server.toolCount,
|
|
149
|
+
source: server.source,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const plugins = input.plugins.filter((p) => matches(p.name));
|
|
153
|
+
const skills = input.skills.filter((s) => matches(s.name)).map(skillRow);
|
|
154
|
+
const mcp = (input.mcpServers ?? []).filter((s) => matches(s.name));
|
|
155
|
+
|
|
156
|
+
const favoritePluginRows = plugins.filter((p) => input.favorites.plugins.includes(p.id)).map(pluginRow);
|
|
157
|
+
const favoriteSkillRows = skills.filter((s) => s.favorite);
|
|
158
|
+
const attention = mcp.filter((s) => s.status === "failed" || s.status === "authNeeded").map(mcpRow);
|
|
159
|
+
const rest = plugins.filter((p) => !input.favorites.plugins.includes(p.id));
|
|
160
|
+
const stale = rest.filter((p) => {
|
|
161
|
+
const last = pluginLastUsed(p, input.usage);
|
|
162
|
+
return last === undefined || now - last > RECENT_MS;
|
|
163
|
+
});
|
|
164
|
+
const active = rest.filter((p) => !stale.includes(p));
|
|
165
|
+
const healthyMcp = mcp.filter((s) => s.status === "connected" || s.status === "connecting").map(mcpRow);
|
|
166
|
+
const normalSkills = skills.filter((s) => !s.favorite);
|
|
167
|
+
|
|
168
|
+
const rows: InstalledRow[] = [];
|
|
169
|
+
const section = (title: string, content: InstalledRow[]) => {
|
|
170
|
+
if (content.length > 0) rows.push({ kind: "section", title }, ...content);
|
|
171
|
+
};
|
|
172
|
+
section("Favorites", [...favoritePluginRows, ...favoriteSkillRows]);
|
|
173
|
+
section("Needs attention", attention);
|
|
174
|
+
section("Not used recently", stale.map(pluginRow));
|
|
175
|
+
section("Plugins", active.map(pluginRow));
|
|
176
|
+
section("MCP servers", healthyMcp);
|
|
177
|
+
section("Skills", normalSkills);
|
|
178
|
+
return rows;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type MarketplaceRow =
|
|
182
|
+
| { kind: "add" }
|
|
183
|
+
| {
|
|
184
|
+
kind: "marketplace";
|
|
185
|
+
name: string;
|
|
186
|
+
sourceLine: string;
|
|
187
|
+
available?: number;
|
|
188
|
+
installedCount: number;
|
|
189
|
+
updated: string;
|
|
190
|
+
official: boolean;
|
|
191
|
+
error?: string;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function sourceLine(known: KnownMarketplace): string {
|
|
195
|
+
const { source } = known;
|
|
196
|
+
switch (source.source) {
|
|
197
|
+
case "github":
|
|
198
|
+
return source.repo;
|
|
199
|
+
case "git":
|
|
200
|
+
return source.url;
|
|
201
|
+
case "url":
|
|
202
|
+
return source.url;
|
|
203
|
+
case "file":
|
|
204
|
+
case "directory":
|
|
205
|
+
return source.path;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildMarketplaceRows(input: {
|
|
210
|
+
known: Record<string, KnownMarketplace>;
|
|
211
|
+
snapshots: Map<string, MarketplaceSnapshot>;
|
|
212
|
+
installedIds: Set<string>;
|
|
213
|
+
}): MarketplaceRow[] {
|
|
214
|
+
const rows: MarketplaceRow[] = [{ kind: "add" }];
|
|
215
|
+
for (const [name, known] of Object.entries(input.known).sort(([a], [b]) => a.localeCompare(b))) {
|
|
216
|
+
const snapshot = input.snapshots.get(name);
|
|
217
|
+
const installedCount = [...input.installedIds].filter((id) => id.endsWith(`@${name}`)).length;
|
|
218
|
+
rows.push({
|
|
219
|
+
kind: "marketplace",
|
|
220
|
+
name,
|
|
221
|
+
sourceLine: sourceLine(known),
|
|
222
|
+
available: snapshot?.manifest?.plugins.length,
|
|
223
|
+
installedCount,
|
|
224
|
+
updated: known.lastUpdated,
|
|
225
|
+
official: name === OFFICIAL_MARKETPLACE_NAME,
|
|
226
|
+
error: snapshot?.errors[0],
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return rows;
|
|
230
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /plugins panel state machine (pure).
|
|
3
|
+
*
|
|
4
|
+
* `applyPanelKey` mutates the state in place (the wiring owns the object and
|
|
5
|
+
* repaints after every key) and returns an effect when the key demands I/O —
|
|
6
|
+
* installs, toggles, marketplace operations — which the wiring executes
|
|
7
|
+
* asynchronously and reflects back through the data it renders.
|
|
8
|
+
*
|
|
9
|
+
* Key routing: navigation keys are always controls. Printable text feeds the
|
|
10
|
+
* search box in Discover (a browse tab: type to filter, Enter to read before
|
|
11
|
+
* installing), the draft in the Add Marketplace dialog, and single-letter
|
|
12
|
+
* actions in Marketplaces (`u` update, `d` remove) and the detail view
|
|
13
|
+
* (`i`/`u`/`e`/`d`/`f`) — the views without a text field.
|
|
14
|
+
*
|
|
15
|
+
* The Installed tab follows the /skills panel instead: Enter and Space act on
|
|
16
|
+
* the row in place (toggle a plugin or skill; open an MCP server's detail,
|
|
17
|
+
* which has no toggle), search sits behind `/` (Esc leaves it), and the free
|
|
18
|
+
* letters act directly — `v` view, `e`/`d` set, `u` uninstall, `f` favorite.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PanelKey } from "./keys.ts";
|
|
22
|
+
import type { DiscoverRow, InstalledRow, MarketplaceRow } from "./rows.ts";
|
|
23
|
+
|
|
24
|
+
export type Tab = "discover" | "installed" | "marketplaces" | "errors";
|
|
25
|
+
export const TABS: Tab[] = ["discover", "installed", "marketplaces", "errors"];
|
|
26
|
+
|
|
27
|
+
export type DetailRef =
|
|
28
|
+
| { kind: "discover"; id: string }
|
|
29
|
+
| { kind: "plugin"; id: string }
|
|
30
|
+
| { kind: "skill"; overrideKey: string }
|
|
31
|
+
| { kind: "mcp"; name: string };
|
|
32
|
+
|
|
33
|
+
export interface PanelState {
|
|
34
|
+
tab: Tab;
|
|
35
|
+
cursor: Record<Tab, number>;
|
|
36
|
+
search: { discover: string; installed: string };
|
|
37
|
+
/** Installed tab: `/` opened the search box, so typed text feeds it. */
|
|
38
|
+
installedSearching: boolean;
|
|
39
|
+
addDialog?: { draft: string };
|
|
40
|
+
detail?: DetailRef;
|
|
41
|
+
restartNeeded: boolean;
|
|
42
|
+
notice?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PanelView {
|
|
46
|
+
discover: DiscoverRow[];
|
|
47
|
+
installed: InstalledRow[];
|
|
48
|
+
marketplaces: MarketplaceRow[];
|
|
49
|
+
errors: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PanelEffect =
|
|
53
|
+
| { kind: "close" }
|
|
54
|
+
| { kind: "installToggle"; row: DiscoverRow }
|
|
55
|
+
| { kind: "setPluginEnabled"; id: string; origin: "claude" | "one-code"; enabled: boolean }
|
|
56
|
+
| { kind: "uninstall"; id: string }
|
|
57
|
+
| { kind: "setSkillEnabled"; overrideKey: string; enabled: boolean }
|
|
58
|
+
| { kind: "toggleFavorite"; target: "plugin" | "skill"; key: string }
|
|
59
|
+
| { kind: "addMarketplace"; input: string }
|
|
60
|
+
| { kind: "removeMarketplace"; name: string }
|
|
61
|
+
| { kind: "refreshMarketplace"; name: string };
|
|
62
|
+
|
|
63
|
+
export function initialPanelState(): PanelState {
|
|
64
|
+
return {
|
|
65
|
+
tab: "discover",
|
|
66
|
+
cursor: { discover: 0, installed: 0, marketplaces: 0, errors: 0 },
|
|
67
|
+
search: { discover: "", installed: "" },
|
|
68
|
+
installedSearching: false,
|
|
69
|
+
restartNeeded: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Rows the cursor can land on (Installed section headers are skipped). */
|
|
74
|
+
export function selectableRows(state: PanelState, view: PanelView): Array<DiscoverRow | InstalledRow | MarketplaceRow | string> {
|
|
75
|
+
switch (state.tab) {
|
|
76
|
+
case "discover":
|
|
77
|
+
return view.discover;
|
|
78
|
+
case "installed":
|
|
79
|
+
return view.installed.filter((row) => row.kind !== "section");
|
|
80
|
+
case "marketplaces":
|
|
81
|
+
return view.marketplaces;
|
|
82
|
+
case "errors":
|
|
83
|
+
return view.errors;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function clampPanelState(state: PanelState, view: PanelView): void {
|
|
88
|
+
const max = Math.max(0, selectableRows(state, view).length - 1);
|
|
89
|
+
state.cursor[state.tab] = Math.min(Math.max(0, state.cursor[state.tab]), max);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function selected(state: PanelState, view: PanelView): DiscoverRow | InstalledRow | MarketplaceRow | string | undefined {
|
|
93
|
+
return selectableRows(state, view)[state.cursor[state.tab]];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detailFor(row: DiscoverRow | InstalledRow | MarketplaceRow | string, tab: Tab): DetailRef | undefined {
|
|
97
|
+
if (typeof row === "string") return undefined;
|
|
98
|
+
if (tab === "discover") return { kind: "discover", id: (row as DiscoverRow).id };
|
|
99
|
+
if ("kind" in row) {
|
|
100
|
+
if (row.kind === "plugin") return { kind: "plugin", id: row.id };
|
|
101
|
+
if (row.kind === "skill") return { kind: "skill", overrideKey: row.overrideKey };
|
|
102
|
+
if (row.kind === "mcp") return { kind: "mcp", name: row.name };
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Resolve a detail ref against the CURRENT view (rows shift under async refreshes). */
|
|
108
|
+
export function resolveDetail(ref: DetailRef, view: PanelView): DiscoverRow | InstalledRow | undefined {
|
|
109
|
+
switch (ref.kind) {
|
|
110
|
+
case "discover":
|
|
111
|
+
return view.discover.find((row) => row.id === ref.id);
|
|
112
|
+
case "plugin":
|
|
113
|
+
return view.installed.find((row) => row.kind === "plugin" && row.id === ref.id);
|
|
114
|
+
case "skill":
|
|
115
|
+
return view.installed.find((row) => row.kind === "skill" && row.overrideKey === ref.overrideKey);
|
|
116
|
+
case "mcp":
|
|
117
|
+
return view.installed.find((row) => row.kind === "mcp" && row.name === ref.name);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function detailEffect(state: PanelState, view: PanelView, letter: string): PanelEffect | undefined {
|
|
122
|
+
const ref = state.detail;
|
|
123
|
+
if (!ref) return undefined;
|
|
124
|
+
const row = resolveDetail(ref, view);
|
|
125
|
+
if (!row || typeof row === "string") return undefined;
|
|
126
|
+
|
|
127
|
+
if (ref.kind === "discover") {
|
|
128
|
+
const discover = row as DiscoverRow;
|
|
129
|
+
if (letter === "i" && !discover.installed) return { kind: "installToggle", row: discover };
|
|
130
|
+
if (letter === "u" && discover.installed) return { kind: "installToggle", row: discover };
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return "kind" in row ? installedLetterEffect(row, letter) : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `e`/`d` set, `u` uninstall (One Code-installed only), `f` favorite — shared by the detail view and the Installed list. */
|
|
137
|
+
function installedLetterEffect(row: InstalledRow, letter: string): PanelEffect | undefined {
|
|
138
|
+
if (row.kind === "plugin") {
|
|
139
|
+
if (letter === "e") return { kind: "setPluginEnabled", id: row.id, origin: row.origin, enabled: true };
|
|
140
|
+
if (letter === "d") return { kind: "setPluginEnabled", id: row.id, origin: row.origin, enabled: false };
|
|
141
|
+
if (letter === "u" && row.origin === "one-code") return { kind: "uninstall", id: row.id };
|
|
142
|
+
if (letter === "f") return { kind: "toggleFavorite", target: "plugin", key: row.id };
|
|
143
|
+
}
|
|
144
|
+
if (row.kind === "skill") {
|
|
145
|
+
if (letter === "e") return { kind: "setSkillEnabled", overrideKey: row.overrideKey, enabled: true };
|
|
146
|
+
if (letter === "d") return { kind: "setSkillEnabled", overrideKey: row.overrideKey, enabled: false };
|
|
147
|
+
if (letter === "f") return { kind: "toggleFavorite", target: "skill", key: row.overrideKey };
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Installed tab, Enter or Space: toggle a plugin/skill row in place (the
|
|
154
|
+
* /skills model); an MCP row has no toggle, so open its detail instead.
|
|
155
|
+
*/
|
|
156
|
+
function selectedInstalled(state: PanelState, view: PanelView): Exclude<InstalledRow, { kind: "section" }> | undefined {
|
|
157
|
+
if (state.tab !== "installed") return undefined;
|
|
158
|
+
return view.installed.filter((r) => r.kind !== "section")[state.cursor.installed];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function installedPrimary(state: PanelState, view: PanelView): PanelEffect | undefined {
|
|
162
|
+
const row = selectedInstalled(state, view);
|
|
163
|
+
if (!row) return undefined;
|
|
164
|
+
if (row.kind === "plugin") return { kind: "setPluginEnabled", id: row.id, origin: row.origin, enabled: !row.enabled };
|
|
165
|
+
if (row.kind === "skill") return { kind: "setSkillEnabled", overrideKey: row.overrideKey, enabled: !row.enabled };
|
|
166
|
+
if (row.kind === "mcp") state.detail = { kind: "mcp", name: row.name };
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Installed tab, single letters outside search: `v` view, `e`/`d` set, `u` uninstall, `f` favorite. */
|
|
171
|
+
function installedLetter(state: PanelState, view: PanelView, letter: string): PanelEffect | undefined {
|
|
172
|
+
const row = selectedInstalled(state, view);
|
|
173
|
+
if (!row) return undefined;
|
|
174
|
+
if (letter === "v") {
|
|
175
|
+
state.detail = detailFor(row, "installed");
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return installedLetterEffect(row, letter);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function applyPanelKey(state: PanelState, key: PanelKey, view: PanelView): PanelEffect | undefined {
|
|
182
|
+
state.notice = undefined;
|
|
183
|
+
|
|
184
|
+
// Modal Add Marketplace dialog captures everything.
|
|
185
|
+
if (state.addDialog) {
|
|
186
|
+
const dialog = state.addDialog;
|
|
187
|
+
switch (key.kind) {
|
|
188
|
+
case "text":
|
|
189
|
+
dialog.draft += key.text;
|
|
190
|
+
return undefined;
|
|
191
|
+
case "backspace":
|
|
192
|
+
dialog.draft = dialog.draft.slice(0, -1);
|
|
193
|
+
return undefined;
|
|
194
|
+
case "enter": {
|
|
195
|
+
const input = dialog.draft.trim();
|
|
196
|
+
state.addDialog = undefined;
|
|
197
|
+
return input ? { kind: "addMarketplace", input } : undefined;
|
|
198
|
+
}
|
|
199
|
+
case "back":
|
|
200
|
+
case "close":
|
|
201
|
+
state.addDialog = undefined;
|
|
202
|
+
return undefined;
|
|
203
|
+
default:
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (state.detail) {
|
|
209
|
+
switch (key.kind) {
|
|
210
|
+
case "back":
|
|
211
|
+
state.detail = undefined;
|
|
212
|
+
return undefined;
|
|
213
|
+
case "close":
|
|
214
|
+
return { kind: "close" };
|
|
215
|
+
case "enter":
|
|
216
|
+
case "space": {
|
|
217
|
+
const row = resolveDetail(state.detail, view);
|
|
218
|
+
if (row && "kind" in row) {
|
|
219
|
+
if (row.kind === "plugin") {
|
|
220
|
+
return { kind: "setPluginEnabled", id: row.id, origin: row.origin, enabled: !row.enabled };
|
|
221
|
+
}
|
|
222
|
+
if (row.kind === "skill") {
|
|
223
|
+
return { kind: "setSkillEnabled", overrideKey: row.overrideKey, enabled: !row.enabled };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
case "text":
|
|
229
|
+
return key.text.length === 1 ? detailEffect(state, view, key.text.toLowerCase()) : undefined;
|
|
230
|
+
default:
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
switch (key.kind) {
|
|
236
|
+
case "close":
|
|
237
|
+
return { kind: "close" };
|
|
238
|
+
case "back": {
|
|
239
|
+
if (state.tab === "discover" && state.search.discover) {
|
|
240
|
+
state.search.discover = "";
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
if (state.tab === "installed" && (state.installedSearching || state.search.installed)) {
|
|
244
|
+
state.installedSearching = false;
|
|
245
|
+
state.search.installed = "";
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return { kind: "close" };
|
|
249
|
+
}
|
|
250
|
+
case "nextTab":
|
|
251
|
+
case "prevTab": {
|
|
252
|
+
const delta = key.kind === "nextTab" ? 1 : -1;
|
|
253
|
+
state.tab = TABS[(TABS.indexOf(state.tab) + delta + TABS.length) % TABS.length];
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
case "up":
|
|
257
|
+
case "down":
|
|
258
|
+
case "pageUp":
|
|
259
|
+
case "pageDown": {
|
|
260
|
+
const step = key.kind === "up" ? -1 : key.kind === "down" ? 1 : key.kind === "pageUp" ? -10 : 10;
|
|
261
|
+
state.cursor[state.tab] += step;
|
|
262
|
+
clampPanelState(state, view);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
case "enter": {
|
|
266
|
+
const row = selected(state, view);
|
|
267
|
+
if (row === undefined) return undefined;
|
|
268
|
+
if (state.tab === "marketplaces") {
|
|
269
|
+
if (typeof row !== "string" && "kind" in row && row.kind === "add") {
|
|
270
|
+
state.addDialog = { draft: "" };
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
if (typeof row !== "string" && "kind" in row && row.kind === "marketplace") {
|
|
274
|
+
state.notice = `${row.name} — ${row.sourceLine}${row.error ? ` — ${row.error}` : ""}`;
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
if (state.tab === "installed") return installedPrimary(state, view);
|
|
280
|
+
const ref = detailFor(row, state.tab);
|
|
281
|
+
if (ref) state.detail = ref;
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
case "space": {
|
|
285
|
+
const row = selected(state, view);
|
|
286
|
+
if (row === undefined || typeof row === "string") return undefined;
|
|
287
|
+
if (state.tab === "discover") return { kind: "installToggle", row: row as DiscoverRow };
|
|
288
|
+
if (state.tab === "installed") return installedPrimary(state, view);
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
case "backspace": {
|
|
292
|
+
if (state.tab === "discover") state.search.discover = state.search.discover.slice(0, -1);
|
|
293
|
+
else if (state.tab === "installed" && state.installedSearching) {
|
|
294
|
+
state.search.installed = state.search.installed.slice(0, -1);
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
case "text": {
|
|
299
|
+
if (state.tab === "discover") {
|
|
300
|
+
state.search.discover += key.text;
|
|
301
|
+
state.cursor.discover = 0;
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
if (state.tab === "installed") {
|
|
305
|
+
if (state.installedSearching) {
|
|
306
|
+
state.search.installed += key.text;
|
|
307
|
+
state.cursor.installed = 0;
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
if (key.text === "/") {
|
|
311
|
+
state.installedSearching = true;
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return key.text.length === 1 ? installedLetter(state, view, key.text.toLowerCase()) : undefined;
|
|
315
|
+
}
|
|
316
|
+
if (state.tab === "marketplaces" && key.text.length === 1) {
|
|
317
|
+
const letter = key.text.toLowerCase();
|
|
318
|
+
const row = selected(state, view);
|
|
319
|
+
if (letter === "a") {
|
|
320
|
+
state.addDialog = { draft: "" };
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
if (row && typeof row !== "string" && "kind" in row && row.kind === "marketplace") {
|
|
324
|
+
if (letter === "u") return { kind: "refreshMarketplace", name: row.name };
|
|
325
|
+
if (letter === "d") return { kind: "removeMarketplace", name: row.name };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|