From 3c4c90f29efd331f9963e3eb0d2d2728fc722bfa Mon Sep 17 00:00:00 2001 From: Dillan Laughlin Date: Tue, 23 Jun 2026 16:55:40 -0500 Subject: [PATCH 1/3] Add failing test for browser tab title resolver (RED) (#1055) Adds resolveBrowserTabTitle() plus its spec. The stub returns the raw value, so the tests fail on purpose: a blank/whitespace value should fall back to "KIP" and a set value should be trimmed. --- .../core/utils/browser-tab-title.util.spec.ts | 16 ++++++++++++++++ src/app/core/utils/browser-tab-title.util.ts | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/app/core/utils/browser-tab-title.util.spec.ts create mode 100644 src/app/core/utils/browser-tab-title.util.ts diff --git a/src/app/core/utils/browser-tab-title.util.spec.ts b/src/app/core/utils/browser-tab-title.util.spec.ts new file mode 100644 index 00000000..ae7fdedd --- /dev/null +++ b/src/app/core/utils/browser-tab-title.util.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +import { resolveBrowserTabTitle } from './browser-tab-title.util'; + +describe('resolveBrowserTabTitle (#1055)', () => { + it('defaults to KIP when the value is missing or blank', () => { + expect(resolveBrowserTabTitle(undefined)).toBe('KIP'); + expect(resolveBrowserTabTitle(null)).toBe('KIP'); + expect(resolveBrowserTabTitle('')).toBe('KIP'); + expect(resolveBrowserTabTitle(' ')).toBe('KIP'); + }); + + it('uses the trimmed configured value when set', () => { + expect(resolveBrowserTabTitle('Mast-KIP')).toBe('Mast-KIP'); + expect(resolveBrowserTabTitle(' Port Engine ')).toBe('Port Engine'); + }); +}); diff --git a/src/app/core/utils/browser-tab-title.util.ts b/src/app/core/utils/browser-tab-title.util.ts new file mode 100644 index 00000000..ad189a36 --- /dev/null +++ b/src/app/core/utils/browser-tab-title.util.ts @@ -0,0 +1,8 @@ +/** + * Resolves the browser tab title (document.title) from the user-configured value. + * Falls back to 'KIP' when the value is empty/whitespace so the tab is never blank (#1055). + */ +export function resolveBrowserTabTitle(value?: string | null): string { + // RED stub: returns the raw value (no default / no trim) - replaced by the fix. + return (value ?? '') as string; +} From c1f895778aa5b6ba9daf594ef12ae5b7b65647dd Mon Sep 17 00:00:00 2001 From: Dillan Laughlin Date: Tue, 23 Jun 2026 16:59:50 -0500 Subject: [PATCH 2/3] Add configurable browser tab title (GREEN) (#1055) resolveBrowserTabTitle() now trims the value and falls back to "KIP" when blank. Adds a persisted browserTabTitle app setting (mirrors the instanceName pattern in SettingsService, defaulting to "KIP"), a "Browser Tab" field in Settings > Display, and an effect in AppComponent that keeps document.title in sync via Angular's Title service. Lets users running several KIP instances tell their tabs apart. --- src/app/app.component.ts | 9 ++++++ .../options/display/display.component.html | 20 +++++++++++++ .../options/display/display.component.ts | 3 ++ .../interfaces/app-settings.interfaces.ts | 1 + src/app/core/services/settings.service.ts | 30 ++++++++++++++++++- src/app/core/utils/browser-tab-title.util.ts | 4 +-- src/default-config/config.blank.const.ts | 3 +- 7 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3f873892..bdbb6583 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -29,6 +29,8 @@ import { ToastService } from './core/services/toast.service'; import { AppNetworkInitService, IBootstrapIssue } from './core/services/app-initNetwork.service'; import { SsoRedirectService } from './core/services/sso-redirect.service'; import { DashboardHistorySeriesSyncService } from './core/services/dashboard-history-series-sync.service'; +import { Title } from '@angular/platform-browser'; +import { resolveBrowserTabTitle } from './core/utils/browser-tab-title.util'; @Component({ selector: 'app-root', @@ -64,6 +66,8 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy { private readonly _uiEvent = inject(uiEventService); private readonly _dialog = inject(DialogService); public readonly settings = inject(SettingsService); + private readonly _titleService = inject(Title); + private readonly _browserTabTitle = toSignal(this.settings.getBrowserTabTitleAsO(), { initialValue: 'KIP' }); private readonly _responsive = inject(BreakpointObserver); private readonly _destroyRef = inject(DestroyRef); private readonly _notificationOverlay = inject(NotificationOverlayService); @@ -92,6 +96,11 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy { private readonly _hotkeyHandler = (key: string) => this.handleKeyDown(key); constructor() { + // Keep the browser tab title (document.title) in sync with the user setting (#1055). + effect(() => { + this._titleService.setTitle(resolveBrowserTabTitle(this._browserTabTitle())); + }); + effect(() => { if (this.settings.configUpgrade()) { const liveVersion = this.settings.getConfigVersion(); diff --git a/src/app/core/components/options/display/display.component.html b/src/app/core/components/options/display/display.component.html index 46a2927c..f4ec6afd 100644 --- a/src/app/core/components/options/display/display.component.html +++ b/src/app/core/components/options/display/display.component.html @@ -55,6 +55,26 @@ Enable light theme for brighter display settings. + + + + Browser Tab + + @if (!isPhonePortrait().matches) { + + Set the browser tab title to tell multiple KIP instances apart. + + } + + + Browser tab title + + + diff --git a/src/app/core/components/options/display/display.component.ts b/src/app/core/components/options/display/display.component.ts index 221b9f2d..13dbe55f 100644 --- a/src/app/core/components/options/display/display.component.ts +++ b/src/app/core/components/options/display/display.component.ts @@ -57,6 +57,7 @@ export class SettingsDisplayComponent implements OnInit { protected isLightTheme = model(false); protected isRemoteControl = model(false); protected instanceName = model(''); + protected browserTabTitle = model('KIP'); protected splitShellEnabled = model(false); protected splitShellSide = model<'left' | 'right'>('left'); protected splitShellSwipeDisabled = model(false); @@ -78,6 +79,7 @@ export class SettingsDisplayComponent implements OnInit { this.isRedNightMode.set(this.settings.getRedNightMode()); this.isRemoteControl.set(this.settings.getIsRemoteControl()); this.instanceName.set(this.settings.getInstanceName()); + this.browserTabTitle.set(this.settings.getBrowserTabTitle()); this.splitShellEnabled.set(this.settings.getSplitShellEnabled()); this.splitShellSide.set(this.settings.getSplitShellSide()); this.splitShellSwipeDisabled.set(this.settings.getSplitShellSwipeDisabled()); @@ -127,6 +129,7 @@ export class SettingsDisplayComponent implements OnInit { this.settings.setSplitShellSide(this.splitShellSide()); this.settings.setSplitShellSwipeDisabled(this.splitShellSwipeDisabled()); this.settings.setWidgetHistoryDisabled(this.widgetHistoryDisabled()); + this.settings.setBrowserTabTitle(this.browserTabTitle()); this.settings.setDisablePathValidation(this.isPathValidationDisabled()); if (!this.setKipPluginConfig()) { this.toast.show('Failed to save KIP plugin configuration on server.', 0, false, 'error'); diff --git a/src/app/core/interfaces/app-settings.interfaces.ts b/src/app/core/interfaces/app-settings.interfaces.ts index 27aff150..903986f9 100644 --- a/src/app/core/interfaces/app-settings.interfaces.ts +++ b/src/app/core/interfaces/app-settings.interfaces.ts @@ -49,6 +49,7 @@ export interface IAppConfig { splitShellSwipeDisabled?: boolean; splitShellWidth?: number; widgetHistoryDisabled?: boolean; + browserTabTitle?: string; } export interface IThemeConfig { diff --git a/src/app/core/services/settings.service.ts b/src/app/core/services/settings.service.ts index 2eac04bd..4ad93340 100644 --- a/src/app/core/services/settings.service.ts +++ b/src/app/core/services/settings.service.ts @@ -40,6 +40,7 @@ export class SettingsService { private nightModeBrightness: BehaviorSubject = new BehaviorSubject(1); private isRemoteControl: BehaviorSubject = new BehaviorSubject(false); private instanceName: BehaviorSubject = new BehaviorSubject(''); + private browserTabTitle: BehaviorSubject = new BehaviorSubject('KIP'); private splitShellEnabled: BehaviorSubject = new BehaviorSubject(false); private splitShellSide: BehaviorSubject<'left' | 'right'> = new BehaviorSubject<'left' | 'right'>('left'); private splitShellSwipeDisabled: BehaviorSubject = new BehaviorSubject(false); @@ -248,6 +249,12 @@ export class SettingsService { this._dashboards = this.activeConfig.dashboards; } + if (this.activeConfig.app.browserTabTitle === undefined) { + this.browserTabTitle.next('KIP'); + } else { + this.browserTabTitle.next(this.activeConfig.app.browserTabTitle); + } + if (this.activeConfig.app.splitShellEnabled === undefined) { this.setSplitShellEnabled(false); } else { @@ -457,6 +464,26 @@ export class SettingsService { this.saveConnectionConfigToLocalStorage(); } + // Browser tab title (document.title) + public getBrowserTabTitleAsO() { + return this.browserTabTitle.asObservable(); + } + + public getBrowserTabTitle(): string { + return this.browserTabTitle.getValue(); + } + + public setBrowserTabTitle(title: string) { + this.browserTabTitle.next(title); + const appConf = this.buildAppStorageObject(); + + if (this.useSharedConfig) { + this.storage.patchConfig('IAppConfig', appConf); + } else { + this.saveAppConfigToLocalStorage(); + } + } + public getDisablePathValidation(): boolean { return this.disablePathValidation; } @@ -710,7 +737,8 @@ export class SettingsService { splitShellSide: this.splitShellSide.getValue() ?? 'right', splitShellWidth: this.splitShellWidth.getValue() ?? 0.5, splitShellSwipeDisabled: this.splitShellSwipeDisabled.getValue(), - widgetHistoryDisabled: this.widgetHistoryDisabled.getValue() + widgetHistoryDisabled: this.widgetHistoryDisabled.getValue(), + browserTabTitle: this.browserTabTitle.getValue() } return storageObject; } diff --git a/src/app/core/utils/browser-tab-title.util.ts b/src/app/core/utils/browser-tab-title.util.ts index ad189a36..84f3ebe2 100644 --- a/src/app/core/utils/browser-tab-title.util.ts +++ b/src/app/core/utils/browser-tab-title.util.ts @@ -3,6 +3,6 @@ * Falls back to 'KIP' when the value is empty/whitespace so the tab is never blank (#1055). */ export function resolveBrowserTabTitle(value?: string | null): string { - // RED stub: returns the raw value (no default / no trim) - replaced by the fix. - return (value ?? '') as string; + const trimmed = (value ?? '').trim(); + return trimmed.length ? trimmed : 'KIP'; } diff --git a/src/default-config/config.blank.const.ts b/src/default-config/config.blank.const.ts index 7e5db14d..8216f2da 100644 --- a/src/default-config/config.blank.const.ts +++ b/src/default-config/config.blank.const.ts @@ -14,7 +14,8 @@ export const DefaultAppConfig: IAppConfig = { "splitShellEnabled": true, "splitShellSide": "left", "splitShellSwipeDisabled": false, - "splitShellWidth": 0.5 + "splitShellWidth": 0.5, + "browserTabTitle": "KIP" } export const DefaultThemeConfig: IThemeConfig = { From c8cb3e95896642bacf6918d18e36552669076a78 Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Wed, 1 Jul 2026 00:12:44 +0300 Subject: [PATCH 3/3] fix(branding): default browser tab title to SKip in fork Preserve SKip's tab branding: the upstream #1055 feature drives document.title and defaulted to 'KIP', which would override the fork's 'SKip' title (index.html, manifest). Rebrand all defaults, the resolver fallback, UI copy, and tests to 'SKip'. --- src/app/app.component.ts | 2 +- .../options/display/display.component.html | 4 ++-- .../components/options/display/display.component.ts | 2 +- src/app/core/services/settings.service.ts | 4 ++-- src/app/core/utils/browser-tab-title.util.spec.ts | 12 ++++++------ src/app/core/utils/browser-tab-title.util.ts | 4 ++-- src/default-config/config.blank.const.ts | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index bdbb6583..87ebd18e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy { private readonly _dialog = inject(DialogService); public readonly settings = inject(SettingsService); private readonly _titleService = inject(Title); - private readonly _browserTabTitle = toSignal(this.settings.getBrowserTabTitleAsO(), { initialValue: 'KIP' }); + private readonly _browserTabTitle = toSignal(this.settings.getBrowserTabTitleAsO(), { initialValue: 'SKip' }); private readonly _responsive = inject(BreakpointObserver); private readonly _destroyRef = inject(DestroyRef); private readonly _notificationOverlay = inject(NotificationOverlayService); diff --git a/src/app/core/components/options/display/display.component.html b/src/app/core/components/options/display/display.component.html index f4ec6afd..f0da44ea 100644 --- a/src/app/core/components/options/display/display.component.html +++ b/src/app/core/components/options/display/display.component.html @@ -62,7 +62,7 @@ @if (!isPhonePortrait().matches) { - Set the browser tab title to tell multiple KIP instances apart. + Set the browser tab title to tell multiple SKip instances apart. } @@ -71,7 +71,7 @@ diff --git a/src/app/core/components/options/display/display.component.ts b/src/app/core/components/options/display/display.component.ts index 13dbe55f..9bdf115a 100644 --- a/src/app/core/components/options/display/display.component.ts +++ b/src/app/core/components/options/display/display.component.ts @@ -57,7 +57,7 @@ export class SettingsDisplayComponent implements OnInit { protected isLightTheme = model(false); protected isRemoteControl = model(false); protected instanceName = model(''); - protected browserTabTitle = model('KIP'); + protected browserTabTitle = model('SKip'); protected splitShellEnabled = model(false); protected splitShellSide = model<'left' | 'right'>('left'); protected splitShellSwipeDisabled = model(false); diff --git a/src/app/core/services/settings.service.ts b/src/app/core/services/settings.service.ts index 4ad93340..80499020 100644 --- a/src/app/core/services/settings.service.ts +++ b/src/app/core/services/settings.service.ts @@ -40,7 +40,7 @@ export class SettingsService { private nightModeBrightness: BehaviorSubject = new BehaviorSubject(1); private isRemoteControl: BehaviorSubject = new BehaviorSubject(false); private instanceName: BehaviorSubject = new BehaviorSubject(''); - private browserTabTitle: BehaviorSubject = new BehaviorSubject('KIP'); + private browserTabTitle: BehaviorSubject = new BehaviorSubject('SKip'); private splitShellEnabled: BehaviorSubject = new BehaviorSubject(false); private splitShellSide: BehaviorSubject<'left' | 'right'> = new BehaviorSubject<'left' | 'right'>('left'); private splitShellSwipeDisabled: BehaviorSubject = new BehaviorSubject(false); @@ -250,7 +250,7 @@ export class SettingsService { } if (this.activeConfig.app.browserTabTitle === undefined) { - this.browserTabTitle.next('KIP'); + this.browserTabTitle.next('SKip'); } else { this.browserTabTitle.next(this.activeConfig.app.browserTabTitle); } diff --git a/src/app/core/utils/browser-tab-title.util.spec.ts b/src/app/core/utils/browser-tab-title.util.spec.ts index ae7fdedd..94dce47a 100644 --- a/src/app/core/utils/browser-tab-title.util.spec.ts +++ b/src/app/core/utils/browser-tab-title.util.spec.ts @@ -2,15 +2,15 @@ import { describe, it, expect } from 'vitest'; import { resolveBrowserTabTitle } from './browser-tab-title.util'; describe('resolveBrowserTabTitle (#1055)', () => { - it('defaults to KIP when the value is missing or blank', () => { - expect(resolveBrowserTabTitle(undefined)).toBe('KIP'); - expect(resolveBrowserTabTitle(null)).toBe('KIP'); - expect(resolveBrowserTabTitle('')).toBe('KIP'); - expect(resolveBrowserTabTitle(' ')).toBe('KIP'); + it('defaults to SKip when the value is missing or blank', () => { + expect(resolveBrowserTabTitle(undefined)).toBe('SKip'); + expect(resolveBrowserTabTitle(null)).toBe('SKip'); + expect(resolveBrowserTabTitle('')).toBe('SKip'); + expect(resolveBrowserTabTitle(' ')).toBe('SKip'); }); it('uses the trimmed configured value when set', () => { - expect(resolveBrowserTabTitle('Mast-KIP')).toBe('Mast-KIP'); + expect(resolveBrowserTabTitle('Mast-SKip')).toBe('Mast-SKip'); expect(resolveBrowserTabTitle(' Port Engine ')).toBe('Port Engine'); }); }); diff --git a/src/app/core/utils/browser-tab-title.util.ts b/src/app/core/utils/browser-tab-title.util.ts index 84f3ebe2..93fd7422 100644 --- a/src/app/core/utils/browser-tab-title.util.ts +++ b/src/app/core/utils/browser-tab-title.util.ts @@ -1,8 +1,8 @@ /** * Resolves the browser tab title (document.title) from the user-configured value. - * Falls back to 'KIP' when the value is empty/whitespace so the tab is never blank (#1055). + * Falls back to 'SKip' when the value is empty/whitespace so the tab is never blank (#1055). */ export function resolveBrowserTabTitle(value?: string | null): string { const trimmed = (value ?? '').trim(); - return trimmed.length ? trimmed : 'KIP'; + return trimmed.length ? trimmed : 'SKip'; } diff --git a/src/default-config/config.blank.const.ts b/src/default-config/config.blank.const.ts index 8216f2da..d2e2cbb7 100644 --- a/src/default-config/config.blank.const.ts +++ b/src/default-config/config.blank.const.ts @@ -15,7 +15,7 @@ export const DefaultAppConfig: IAppConfig = { "splitShellSide": "left", "splitShellSwipeDisabled": false, "splitShellWidth": 0.5, - "browserTabTitle": "KIP" + "browserTabTitle": "SKip" } export const DefaultThemeConfig: IThemeConfig = {