[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:
151
domain/sessionTabTitle.ts
Normal file
151
domain/sessionTabTitle.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { TerminalSession } from '../types';
|
||||
import type { CodingCliProviderId } from './codingCliProviders';
|
||||
import { normalizeCodingCliTitle } from './codingCliTitleParse';
|
||||
import type { DynamicTabTitleMode } from './models/terminal';
|
||||
|
||||
/** Static connection label: user rename or host label. */
|
||||
export const getSessionConnectionLabel = (session: Pick<TerminalSession, 'customName' | 'hostLabel'>): string => {
|
||||
return session.customName || session.hostLabel || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Default title given to a freshly created split workspace. Used as a sentinel:
|
||||
* while the workspace still carries this title (i.e. the user has not renamed
|
||||
* it), the tab derives a label from its sessions instead of showing "Workspace".
|
||||
*/
|
||||
export const DEFAULT_WORKSPACE_TITLE = 'Workspace';
|
||||
|
||||
/**
|
||||
* Compose the title for a workspace created by merging standalone session
|
||||
* tabs (e.g. dragging tab "02" onto tab "01"). The merged workspace inherits
|
||||
* both tabs' connection labels joined with "/" ("01/02") so the window title
|
||||
* and the rename dialog show the merged identity instead of the generic
|
||||
* "Workspace". Returns null when neither session has a label — the caller
|
||||
* keeps the default auto-titled behavior in that case. Identical labels
|
||||
* collapse to a single name so two panes on the same host don't read
|
||||
* "web-01/web-01".
|
||||
*/
|
||||
export const buildMergedWorkspaceTitle = (
|
||||
baseLabel: string,
|
||||
joiningLabel: string,
|
||||
): string | null => {
|
||||
const base = baseLabel.trim();
|
||||
const joining = joiningLabel.trim();
|
||||
if (!base && !joining) return null;
|
||||
if (!base) return joining;
|
||||
if (!joining || joining === base) return base;
|
||||
return `${base}/${joining}`;
|
||||
};
|
||||
|
||||
type WorkspaceTabLabelSession = Pick<TerminalSession, 'id' | 'customName' | 'hostLabel' | 'hostId'>;
|
||||
|
||||
/**
|
||||
* Recompute a merged workspace's composed title from its *current* member
|
||||
* sessions. Unlike the one-shot `buildMergedWorkspaceTitle` call at merge
|
||||
* time, this runs over live membership, so pane renames and panes being
|
||||
* added or removed are reflected. Returns null when no member has a label.
|
||||
* Labels are deduplicated by exact match against the other member labels
|
||||
* (not against the accumulated title, which would only catch full-string
|
||||
* repeats like "web/db/web/db"), keeping first-seen order: `web/db/web`
|
||||
* collapses to `web/db`.
|
||||
*/
|
||||
export const buildMergedWorkspaceTitleFromSessions = (
|
||||
sessions: readonly WorkspaceTabLabelSession[],
|
||||
): string | null => {
|
||||
const labels: string[] = [];
|
||||
for (const session of sessions) {
|
||||
const label = getSessionConnectionLabel(session).trim();
|
||||
if (!label || labels.includes(label)) continue;
|
||||
labels.push(label);
|
||||
}
|
||||
if (labels.length === 0) return null;
|
||||
return labels.join('/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the label shown on a split-workspace tab. When the user has renamed
|
||||
* the workspace, that name wins. Otherwise derive it from the focused session's
|
||||
* host (e.g. "Localhost" for a local shell, the host config name for SSH) so the
|
||||
* tab reads as a concrete host instead of the generic "Workspace". If the
|
||||
* workspace holds sessions for more than one distinct host, the focused host is
|
||||
* shown with a "+N" suffix for the others.
|
||||
*
|
||||
* Uses the stable connection label (rename / host label) rather than the
|
||||
* dynamic shell/agent title: those live titles are published only to the
|
||||
* session presentation store (not the plain session records passed here), and
|
||||
* they can differ per-pane on a single host — counting them would both go stale
|
||||
* and produce a bogus "+N". The host identity is what this tab is about.
|
||||
*/
|
||||
export const resolveWorkspaceTabLabel = (
|
||||
workspace: { title?: string; focusedSessionId?: string | null; autoTitle?: boolean },
|
||||
sessions: readonly WorkspaceTabLabelSession[],
|
||||
): string => {
|
||||
const title = workspace.title?.trim();
|
||||
// Derive a host label only for an auto-titled workspace. The `autoTitle` flag
|
||||
// is authoritative when present (so a workspace a user explicitly named
|
||||
// "Workspace" is kept, not overwritten); legacy workspaces without the flag
|
||||
// fall back to matching the default-title string.
|
||||
const isAutoTitle = workspace.autoTitle ?? (title === DEFAULT_WORKSPACE_TITLE);
|
||||
if (!isAutoTitle) {
|
||||
return title || DEFAULT_WORKSPACE_TITLE;
|
||||
}
|
||||
if (sessions.length === 0) {
|
||||
return title || DEFAULT_WORKSPACE_TITLE;
|
||||
}
|
||||
const focused = sessions.find((s) => s.id === workspace.focusedSessionId) ?? sessions[0];
|
||||
const primary = getSessionConnectionLabel(focused);
|
||||
// Count distinct HOSTS by hostId, the only stable host identity: a rename
|
||||
// rewrites both customName and hostLabel (useSessionState.submitSessionRename),
|
||||
// so neither is safe to count by — a renamed pane on the same host would add a
|
||||
// spurious "+1". hostId is shared across all local terminals and per host
|
||||
// config, and is untouched by renames. The focused pane may still show its
|
||||
// rename as the primary label.
|
||||
const distinctHosts = new Set(sessions.map((s) => s.hostId).filter(Boolean));
|
||||
if (distinctHosts.size > 1) {
|
||||
return `${primary} +${distinctHosts.size - 1}`;
|
||||
}
|
||||
return primary || title || DEFAULT_WORKSPACE_TITLE;
|
||||
};
|
||||
|
||||
export const shouldUpdateCodingCliTabIcon = (
|
||||
dynamicTabTitleMode: DynamicTabTitleMode = 'agent',
|
||||
): boolean => dynamicTabTitleMode !== 'off';
|
||||
|
||||
export const resolveCodingCliProviderIconUpdate = ({
|
||||
dynamicTabTitleMode,
|
||||
currentProviderId,
|
||||
nextProviderId,
|
||||
}: {
|
||||
dynamicTabTitleMode: DynamicTabTitleMode;
|
||||
currentProviderId?: CodingCliProviderId;
|
||||
nextProviderId: CodingCliProviderId | null;
|
||||
}): CodingCliProviderId | null | undefined => {
|
||||
if (!shouldUpdateCodingCliTabIcon(dynamicTabTitleMode)) return undefined;
|
||||
if ((currentProviderId ?? null) === nextProviderId) return undefined;
|
||||
return nextProviderId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the label shown on session tabs and pane headers.
|
||||
* Uses the shell-reported title according to the global dynamic title mode.
|
||||
*/
|
||||
export const resolveSessionTabTitle = (
|
||||
session: Pick<TerminalSession, 'customName' | 'hostLabel' | 'dynamicTitle' | 'codingCliProviderId'>,
|
||||
dynamicTabTitleMode: DynamicTabTitleMode = 'agent',
|
||||
): string => {
|
||||
const connectionLabel = getSessionConnectionLabel(session);
|
||||
if (dynamicTabTitleMode === 'off') {
|
||||
return connectionLabel;
|
||||
}
|
||||
if (session.customName) {
|
||||
return session.customName;
|
||||
}
|
||||
if (dynamicTabTitleMode === 'agent' && !session.codingCliProviderId) {
|
||||
return connectionLabel;
|
||||
}
|
||||
const dynamicTitle = session.dynamicTitle?.trim();
|
||||
if (!dynamicTitle) {
|
||||
return connectionLabel;
|
||||
}
|
||||
return normalizeCodingCliTitle(dynamicTitle) || dynamicTitle;
|
||||
};
|
||||
Reference in New Issue
Block a user