[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
147
application/state/pluginViewTabStore.ts
Normal file
147
application/state/pluginViewTabStore.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
import { activeTabStore } from './activeTabStore';
|
||||
|
||||
export const PLUGIN_VIEW_TAB_PREFIX = 'plugin-view:';
|
||||
|
||||
export interface PluginViewTab {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
pluginName: string;
|
||||
viewId: string;
|
||||
title: string;
|
||||
icon?: NetcattyPluginIconReference;
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ClosedPluginViewTabEvent {
|
||||
tab: PluginViewTab;
|
||||
}
|
||||
|
||||
export function toPluginViewTabId(pluginId: string, viewId: string): string {
|
||||
return `${PLUGIN_VIEW_TAB_PREFIX}${pluginId}:${viewId}`;
|
||||
}
|
||||
|
||||
export function isPluginViewTabId(tabId: string): boolean {
|
||||
return tabId.startsWith(PLUGIN_VIEW_TAB_PREFIX);
|
||||
}
|
||||
|
||||
export function resolveBatchTabCloseFocus({
|
||||
orderedTabIds,
|
||||
closingTabIds,
|
||||
activeTabId,
|
||||
}: {
|
||||
orderedTabIds: readonly string[];
|
||||
closingTabIds: ReadonlySet<string>;
|
||||
activeTabId: string;
|
||||
}): string {
|
||||
if (!closingTabIds.has(activeTabId)) return activeTabId;
|
||||
const activeIndex = orderedTabIds.indexOf(activeTabId);
|
||||
if (activeIndex === -1) return 'vault';
|
||||
for (let distance = 1; distance < orderedTabIds.length; distance += 1) {
|
||||
const left = orderedTabIds[activeIndex - distance];
|
||||
if (left && !closingTabIds.has(left)) return left;
|
||||
const right = orderedTabIds[activeIndex + distance];
|
||||
if (right && !closingTabIds.has(right)) return right;
|
||||
}
|
||||
return 'vault';
|
||||
}
|
||||
|
||||
export function resolvePluginViewRequest(
|
||||
requested: { viewId: string; context?: Record<string, unknown> } | null,
|
||||
activeTab: Pick<PluginViewTab, 'viewId' | 'context'> | null,
|
||||
): { viewId: string; context?: Record<string, unknown> } | null {
|
||||
if (requested) return requested;
|
||||
return activeTab ? { viewId: activeTab.viewId, context: activeTab.context } : null;
|
||||
}
|
||||
|
||||
export class PluginViewTabStore {
|
||||
private tabs: readonly PluginViewTab[] = Object.freeze([]);
|
||||
private listeners = new Set<() => void>();
|
||||
private closeListeners = new Set<(event: ClosedPluginViewTabEvent) => void>();
|
||||
|
||||
constructor(private readonly activeTabs: Pick<typeof activeTabStore, 'getActiveTabId' | 'setActiveTabId'> = activeTabStore) {}
|
||||
|
||||
getTabs = () => this.tabs;
|
||||
|
||||
getTab(tabId: string): PluginViewTab | undefined {
|
||||
return this.tabs.find((tab) => tab.id === tabId);
|
||||
}
|
||||
|
||||
open(input: Omit<PluginViewTab, 'id'>): PluginViewTab {
|
||||
const id = toPluginViewTabId(input.pluginId, input.viewId);
|
||||
const tab = Object.freeze({ ...input, id });
|
||||
const index = this.tabs.findIndex((candidate) => candidate.id === id);
|
||||
this.tabs = Object.freeze(index === -1
|
||||
? [...this.tabs, tab]
|
||||
: this.tabs.map((candidate) => candidate.id === id ? tab : candidate));
|
||||
this.emit();
|
||||
this.activeTabs.setActiveTabId(id);
|
||||
return tab;
|
||||
}
|
||||
|
||||
close(tabId: string): void {
|
||||
const closed = this.tabs.find((tab) => tab.id === tabId);
|
||||
if (!closed) return;
|
||||
const next = this.tabs.filter((tab) => tab.id !== tabId);
|
||||
this.tabs = Object.freeze(next);
|
||||
if (this.activeTabs.getActiveTabId() === tabId) this.activeTabs.setActiveTabId('vault');
|
||||
this.emit();
|
||||
this.emitClosed(closed);
|
||||
}
|
||||
|
||||
retain(viewIds: ReadonlySet<string>): void {
|
||||
const next = this.tabs.filter((tab) => viewIds.has(tab.viewId));
|
||||
if (next.length === this.tabs.length) return;
|
||||
const removed = this.tabs.filter((tab) => !viewIds.has(tab.viewId));
|
||||
const activeTabId = this.activeTabs.getActiveTabId();
|
||||
this.tabs = Object.freeze(next);
|
||||
if (isPluginViewTabId(activeTabId) && !next.some((tab) => tab.id === activeTabId)) {
|
||||
this.activeTabs.setActiveTabId('vault');
|
||||
}
|
||||
this.emit();
|
||||
for (const tab of removed) this.emitClosed(tab);
|
||||
}
|
||||
|
||||
refreshMetadata(entries: readonly Omit<PluginViewTab, 'id' | 'context'>[]): void {
|
||||
const metadata = new Map(entries.map((entry) => [toPluginViewTabId(entry.pluginId, entry.viewId), entry]));
|
||||
let changed = false;
|
||||
const next = this.tabs.map((tab) => {
|
||||
const entry = metadata.get(tab.id);
|
||||
if (!entry) return tab;
|
||||
if (entry.pluginName === tab.pluginName
|
||||
&& entry.title === tab.title
|
||||
&& JSON.stringify(entry.icon ?? null) === JSON.stringify(tab.icon ?? null)) return tab;
|
||||
changed = true;
|
||||
return Object.freeze({ ...tab, ...entry, id: tab.id });
|
||||
});
|
||||
if (!changed) return;
|
||||
this.tabs = Object.freeze(next);
|
||||
this.emit();
|
||||
}
|
||||
|
||||
subscribe = (listener: () => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
};
|
||||
|
||||
onDidClose = (listener: (event: ClosedPluginViewTabEvent) => void) => {
|
||||
this.closeListeners.add(listener);
|
||||
return () => this.closeListeners.delete(listener);
|
||||
};
|
||||
|
||||
private emit(): void {
|
||||
for (const listener of this.listeners) listener();
|
||||
}
|
||||
|
||||
private emitClosed(tab: PluginViewTab): void {
|
||||
const event = Object.freeze({ tab });
|
||||
for (const listener of this.closeListeners) listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginViewTabStore = new PluginViewTabStore();
|
||||
|
||||
export function usePluginViewTabs(): readonly PluginViewTab[] {
|
||||
return useSyncExternalStore(pluginViewTabStore.subscribe, pluginViewTabStore.getTabs, pluginViewTabStore.getTabs);
|
||||
}
|
||||
Reference in New Issue
Block a user