tabby-tabbyspaces 0.0.1 → 0.2.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/.claude/settings.local.json +29 -2
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +81 -0
- package/.github/workflows/release.yml +30 -0
- package/CHANGELOG.md +92 -20
- package/CLAUDE.md +196 -15
- package/CONTRIBUTING.md +3 -1
- package/README.md +80 -61
- package/RELEASE.md +91 -0
- package/TODO.md +77 -0
- package/dist/build-config.d.ts +3 -3
- package/dist/components/deleteConfirmModal.component.d.ts +7 -0
- package/dist/components/deleteConfirmModal.component.d.ts.map +1 -0
- package/dist/components/paneEditor.component.d.ts +9 -13
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +50 -35
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +61 -28
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +56 -27
- package/dist/components/workspaceList.component.d.ts.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.LICENSE.txt +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models/workspace.model.d.ts +118 -76
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +26 -0
- package/dist/providers/config.provider.d.ts +8 -8
- package/dist/providers/settings.provider.d.ts +7 -7
- package/dist/providers/settings.provider.d.ts.map +1 -1
- package/dist/providers/toolbar.provider.d.ts +23 -12
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +28 -0
- package/dist/services/startupCommand.service.d.ts.map +1 -0
- package/dist/services/workspaceBackground.service.d.ts +38 -0
- package/dist/services/workspaceBackground.service.d.ts.map +1 -0
- package/dist/services/workspaceEditor.service.d.ts +46 -24
- package/dist/services/workspaceEditor.service.d.ts.map +1 -1
- package/docs/DESIGN.md +57 -0
- package/docs/SESSION-2026-01-14-S1-DESIGN.md +134 -0
- package/docs/marketing_status.md +92 -0
- package/mockups/index.html +162 -0
- package/mockups/s1-tight-sharp.html +522 -0
- package/mockups/shared/base.css +216 -0
- package/mockups/v06-tabbed.html +643 -0
- package/package.json +3 -7
- package/screenshots/editor.png +0 -0
- package/screenshots/pane-edit.png +0 -0
- package/scripts/build-dev.js +2 -1
- package/scripts/build-prod.js +40 -0
- package/src/components/deleteConfirmModal.component.ts +23 -0
- package/src/components/paneEditor.component.pug +27 -43
- package/src/components/paneEditor.component.scss +37 -85
- package/src/components/paneEditor.component.ts +6 -16
- package/src/components/splitPreview.component.pug +36 -5
- package/src/components/splitPreview.component.scss +78 -45
- package/src/components/splitPreview.component.ts +83 -18
- package/src/components/workspaceEditor.component.pug +162 -74
- package/src/components/workspaceEditor.component.scss +261 -108
- package/src/components/workspaceEditor.component.ts +294 -31
- package/src/components/workspaceList.component.pug +32 -41
- package/src/components/workspaceList.component.scss +89 -74
- package/src/components/workspaceList.component.ts +181 -44
- package/src/index.ts +6 -0
- package/src/models/workspace.model.ts +113 -8
- package/src/providers/settings.provider.ts +2 -2
- package/src/providers/toolbar.provider.ts +113 -13
- package/src/services/startupCommand.service.ts +140 -0
- package/src/services/workspaceBackground.service.ts +167 -0
- package/src/services/workspaceEditor.service.ts +134 -65
- package/src/styles/_index.scss +3 -0
- package/src/styles/_mixins.scss +180 -0
- package/src/styles/_variables.scss +67 -0
- package/RELEASE_PLAN.md +0 -161
- package/screenshots/workspace-edit.png +0 -0
|
@@ -1,29 +1,122 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core'
|
|
2
|
-
import { ToolbarButtonProvider, ToolbarButton, ProfilesService, AppService } from 'tabby-core'
|
|
2
|
+
import { ToolbarButtonProvider, ToolbarButton, ProfilesService, AppService, SplitTabComponent } from 'tabby-core'
|
|
3
|
+
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
|
3
4
|
import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
5
|
+
import { StartupCommandService } from '../services/startupCommand.service'
|
|
6
|
+
import { WorkspaceBackgroundService } from '../services/workspaceBackground.service'
|
|
4
7
|
import { SettingsTabComponent } from 'tabby-settings'
|
|
5
|
-
import { CONFIG_KEY, DISPLAY_NAME } from '../build-config'
|
|
8
|
+
import { CONFIG_KEY, DISPLAY_NAME, IS_DEV } from '../build-config'
|
|
9
|
+
|
|
10
|
+
const ICON_GRID = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
11
|
+
<rect x="3" y="3" width="7" height="7"/>
|
|
12
|
+
<rect x="14" y="3" width="7" height="7"/>
|
|
13
|
+
<rect x="14" y="14" width="7" height="7"/>
|
|
14
|
+
<rect x="3" y="14" width="7" height="7"/>
|
|
15
|
+
</svg>`
|
|
16
|
+
|
|
17
|
+
const ICON_BOLT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
18
|
+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
19
|
+
</svg>`
|
|
20
|
+
|
|
21
|
+
const SELECTOR_SETTINGS_ID = '__settings__'
|
|
22
|
+
|
|
6
23
|
import { countPanes } from '../models/workspace.model'
|
|
7
24
|
|
|
25
|
+
/** Recovery token structure for workspace tabs */
|
|
26
|
+
interface RecoveryTokenWithWorkspace {
|
|
27
|
+
workspaceId?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
@Injectable()
|
|
9
31
|
export class WorkspaceToolbarProvider extends ToolbarButtonProvider {
|
|
10
32
|
constructor(
|
|
11
33
|
private workspaceService: WorkspaceEditorService,
|
|
12
34
|
private profilesService: ProfilesService,
|
|
13
|
-
private app: AppService
|
|
35
|
+
private app: AppService,
|
|
36
|
+
private startupService: StartupCommandService,
|
|
37
|
+
private backgroundService: WorkspaceBackgroundService
|
|
14
38
|
) {
|
|
15
39
|
super()
|
|
40
|
+
// Initialize background service to listen for tab events
|
|
41
|
+
this.backgroundService.initialize()
|
|
42
|
+
|
|
43
|
+
// Wait for Tabby to finish recovery before launching startup workspaces
|
|
44
|
+
this.waitForTabbyReady().then(() => {
|
|
45
|
+
this.workspaceService.cleanupOrphanedProfiles()
|
|
46
|
+
this.launchStartupWorkspaces()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private waitForTabbyReady(): Promise<void> {
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
let lastTabCount = -1
|
|
53
|
+
const checkStable = () => {
|
|
54
|
+
const currentCount = this.app.tabs.length
|
|
55
|
+
if (currentCount === lastTabCount && currentCount >= 0) {
|
|
56
|
+
resolve()
|
|
57
|
+
} else {
|
|
58
|
+
lastTabCount = currentCount
|
|
59
|
+
setTimeout(checkStable, 300)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Initial delay to let Tabby start loading
|
|
63
|
+
setTimeout(checkStable, 500)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async launchStartupWorkspaces(): Promise<void> {
|
|
68
|
+
const workspaces = this.workspaceService.getWorkspaces()
|
|
69
|
+
const startupWorkspaces = workspaces.filter(w => w.launchOnStartup)
|
|
70
|
+
|
|
71
|
+
for (const workspace of startupWorkspaces) {
|
|
72
|
+
if (this.isWorkspaceAlreadyOpen(workspace.id)) {
|
|
73
|
+
console.log(`[TabbySpaces] Workspace "${workspace.name}" already open, skipping`)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
await this.openWorkspace(workspace.id)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Type-safe helper to extract workspace ID from tab's recovery token.
|
|
82
|
+
*/
|
|
83
|
+
private getRecoveryWorkspaceId(tab: unknown): string | undefined {
|
|
84
|
+
if (tab && typeof tab === 'object' && 'recoveryToken' in tab) {
|
|
85
|
+
const token = (tab as { recoveryToken?: RecoveryTokenWithWorkspace }).recoveryToken
|
|
86
|
+
return token?.workspaceId
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private isWorkspaceAlreadyOpen(workspaceId: string): boolean {
|
|
92
|
+
const profilePrefix = `split-layout:${CONFIG_KEY}:`
|
|
93
|
+
|
|
94
|
+
for (const tab of this.app.tabs) {
|
|
95
|
+
if (tab instanceof SplitTabComponent) {
|
|
96
|
+
// Strategy 1: Check recoveryToken.workspaceId (for restored tabs)
|
|
97
|
+
if (this.getRecoveryWorkspaceId(tab) === workspaceId) {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Strategy 2: Check profile ID (for freshly opened tabs)
|
|
102
|
+
for (const child of tab.getAllTabs()) {
|
|
103
|
+
if (child instanceof BaseTerminalTabComponent) {
|
|
104
|
+
const profileId = child.profile?.id ?? ''
|
|
105
|
+
// Strict matching: prefix + workspaceId at the end
|
|
106
|
+
if (profileId.startsWith(profilePrefix) && profileId.endsWith(`:${workspaceId}`)) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false
|
|
16
114
|
}
|
|
17
115
|
|
|
18
116
|
provide(): ToolbarButton[] {
|
|
19
117
|
return [
|
|
20
118
|
{
|
|
21
|
-
icon:
|
|
22
|
-
<rect x="3" y="3" width="7" height="7"/>
|
|
23
|
-
<rect x="14" y="3" width="7" height="7"/>
|
|
24
|
-
<rect x="14" y="14" width="7" height="7"/>
|
|
25
|
-
<rect x="3" y="14" width="7" height="7"/>
|
|
26
|
-
</svg>`,
|
|
119
|
+
icon: IS_DEV ? ICON_BOLT : ICON_GRID,
|
|
27
120
|
title: DISPLAY_NAME,
|
|
28
121
|
weight: 5,
|
|
29
122
|
click: () => this.showWorkspaceSelector()
|
|
@@ -53,12 +146,12 @@ export class WorkspaceToolbarProvider extends ToolbarButtonProvider {
|
|
|
53
146
|
description: 'Create and edit workspaces',
|
|
54
147
|
icon: 'cog',
|
|
55
148
|
color: undefined,
|
|
56
|
-
result:
|
|
149
|
+
result: SELECTOR_SETTINGS_ID
|
|
57
150
|
})
|
|
58
151
|
|
|
59
152
|
const selectedId = await this.app.showSelector('Select Workspace', options)
|
|
60
153
|
|
|
61
|
-
if (selectedId ===
|
|
154
|
+
if (selectedId === SELECTOR_SETTINGS_ID) {
|
|
62
155
|
this.openSettings()
|
|
63
156
|
} else if (selectedId) {
|
|
64
157
|
this.openWorkspace(selectedId)
|
|
@@ -69,13 +162,20 @@ export class WorkspaceToolbarProvider extends ToolbarButtonProvider {
|
|
|
69
162
|
this.app.openNewTabRaw({ type: SettingsTabComponent, inputs: { activeTab: CONFIG_KEY } })
|
|
70
163
|
}
|
|
71
164
|
|
|
72
|
-
private openWorkspace(workspaceId: string): void {
|
|
165
|
+
private async openWorkspace(workspaceId: string): Promise<void> {
|
|
73
166
|
const workspaces = this.workspaceService.getWorkspaces()
|
|
74
167
|
const workspace = workspaces.find((w) => w.id === workspaceId)
|
|
75
168
|
|
|
76
169
|
if (!workspace) return
|
|
77
170
|
|
|
78
|
-
|
|
171
|
+
// Register startup commands BEFORE opening the workspace
|
|
172
|
+
// Commands will be sent via sendInput() when terminals open
|
|
173
|
+
const commands = this.workspaceService.collectStartupCommands(workspace)
|
|
174
|
+
if (commands.length > 0) {
|
|
175
|
+
this.startupService.registerCommands(commands)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const profile = await this.workspaceService.generateTabbyProfile(workspace)
|
|
79
179
|
this.profilesService.openNewTabForProfile(profile)
|
|
80
180
|
}
|
|
81
181
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core'
|
|
2
|
+
import { AppService, BaseTabComponent, SplitTabComponent } from 'tabby-core'
|
|
3
|
+
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
|
4
|
+
import { first, timeout, of } from 'rxjs'
|
|
5
|
+
import { catchError } from 'rxjs/operators'
|
|
6
|
+
|
|
7
|
+
export interface PendingCommand {
|
|
8
|
+
paneId: string
|
|
9
|
+
command?: string
|
|
10
|
+
originalTitle: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handles startup commands for workspace panes.
|
|
15
|
+
*
|
|
16
|
+
* This service listens to tab open events and sends startup commands
|
|
17
|
+
* to terminals that match registered pane IDs.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: This is a module-level singleton that lives for the app lifetime.
|
|
20
|
+
* The tabOpened$ subscription intentionally runs forever - no cleanup needed.
|
|
21
|
+
*/
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class StartupCommandService {
|
|
24
|
+
private pendingCommands: Map<string, PendingCommand> = new Map()
|
|
25
|
+
|
|
26
|
+
constructor(private app: AppService) {
|
|
27
|
+
this.app.tabOpened$.subscribe((tab) => this.onTabOpened(tab))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
registerCommands(commands: PendingCommand[]): void {
|
|
31
|
+
console.log('[TabbySpaces] Registering commands:', commands)
|
|
32
|
+
for (const cmd of commands) {
|
|
33
|
+
this.pendingCommands.set(cmd.paneId, cmd)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private onTabOpened(tab: BaseTabComponent): void {
|
|
38
|
+
console.log('[TabbySpaces] Tab opened:', {
|
|
39
|
+
type: tab.constructor.name,
|
|
40
|
+
title: tab.title,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Handle SplitTabComponent - get all child terminal tabs
|
|
44
|
+
if (tab instanceof SplitTabComponent) {
|
|
45
|
+
console.log('[TabbySpaces] SplitTabComponent detected, waiting for children...')
|
|
46
|
+
// Wait for split tab to fully initialize its children
|
|
47
|
+
setTimeout(() => this.processChildTabs(tab), 300)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle individual terminal tab (shouldn't happen for split-layout, but just in case)
|
|
52
|
+
if (tab instanceof BaseTerminalTabComponent) {
|
|
53
|
+
this.processTerminalTab(tab)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private processChildTabs(splitTab: SplitTabComponent): void {
|
|
58
|
+
// Get all nested tabs from the split container
|
|
59
|
+
const allTabs = splitTab.getAllTabs()
|
|
60
|
+
console.log('[TabbySpaces] Found child tabs:', allTabs.length)
|
|
61
|
+
|
|
62
|
+
for (const tab of allTabs) {
|
|
63
|
+
if (tab instanceof BaseTerminalTabComponent) {
|
|
64
|
+
this.processTerminalTab(tab)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
private processTerminalTab(terminalTab: BaseTerminalTabComponent<any>): void {
|
|
71
|
+
const paneId = terminalTab.customTitle || terminalTab.title
|
|
72
|
+
console.log('[TabbySpaces] Processing terminal tab:', {
|
|
73
|
+
title: terminalTab.title,
|
|
74
|
+
customTitle: terminalTab.customTitle,
|
|
75
|
+
paneId,
|
|
76
|
+
pendingKeys: [...this.pendingCommands.keys()],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const pending = this.pendingCommands.get(paneId)
|
|
80
|
+
if (!pending) {
|
|
81
|
+
console.log('[TabbySpaces] No matching command for paneId:', paneId)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.pendingCommands.delete(paneId)
|
|
86
|
+
|
|
87
|
+
// Build startup command (cd + command)
|
|
88
|
+
const fullCommand = this.buildFullCommand(pending)
|
|
89
|
+
if (!fullCommand) {
|
|
90
|
+
console.log('[TabbySpaces] No command to send (no cwd or startup command)')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('[TabbySpaces] Command matched, waiting for shell output...:', fullCommand)
|
|
95
|
+
|
|
96
|
+
// Unified command sender - reduces duplication
|
|
97
|
+
const sendCommand = () => {
|
|
98
|
+
console.log('[TabbySpaces] Shell ready, sending command:', fullCommand)
|
|
99
|
+
terminalTab.sendInput(fullCommand + '\r')
|
|
100
|
+
this.clearProfileArgs(terminalTab)
|
|
101
|
+
this.setTabTitle(terminalTab, pending.originalTitle)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wait for shell to emit first output (prompt), then send command
|
|
105
|
+
if (terminalTab.session?.output$) {
|
|
106
|
+
terminalTab.session.output$.pipe(
|
|
107
|
+
first(),
|
|
108
|
+
timeout(2000), // Prevent infinite wait if shell doesn't emit
|
|
109
|
+
catchError(() => of(null)) // Fallback on timeout/error
|
|
110
|
+
).subscribe(() => {
|
|
111
|
+
// Small delay after prompt renders
|
|
112
|
+
setTimeout(sendCommand, 100)
|
|
113
|
+
})
|
|
114
|
+
} else {
|
|
115
|
+
console.log('[TabbySpaces] No session.output$, falling back to timeout')
|
|
116
|
+
setTimeout(sendCommand, 500)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private buildFullCommand(pending: PendingCommand): string | null {
|
|
121
|
+
return pending.command || null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
private clearProfileArgs(terminalTab: BaseTerminalTabComponent<any>): void {
|
|
126
|
+
// Clear args from profile to prevent native splits from re-running startup command
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
const profile = (terminalTab as any).profile
|
|
129
|
+
if (profile?.options?.args) {
|
|
130
|
+
console.log('[TabbySpaces] Clearing profile args to prevent re-run on split')
|
|
131
|
+
profile.options.args = []
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
private setTabTitle(terminalTab: BaseTerminalTabComponent<any>, title: string): void {
|
|
137
|
+
terminalTab.setTitle(title)
|
|
138
|
+
terminalTab.customTitle = title
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core'
|
|
2
|
+
import { AppService, SplitTabComponent } from 'tabby-core'
|
|
3
|
+
import { WorkspaceEditorService } from './workspaceEditor.service'
|
|
4
|
+
import { WorkspaceBackground } from '../models/workspace.model'
|
|
5
|
+
import { CONFIG_KEY } from '../build-config'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Service for applying custom backgrounds to workspace tabs.
|
|
9
|
+
* Injects CSS dynamically based on workspace configuration.
|
|
10
|
+
*/
|
|
11
|
+
@Injectable({ providedIn: 'root' })
|
|
12
|
+
export class WorkspaceBackgroundService {
|
|
13
|
+
private styleElement: HTMLStyleElement | null = null
|
|
14
|
+
private appliedWorkspaces = new Map<string, string>() // workspaceId -> CSS
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private app: AppService,
|
|
18
|
+
private workspaceService: WorkspaceEditorService
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the service by setting up tab event listeners.
|
|
23
|
+
* Must be called once during app initialization.
|
|
24
|
+
*/
|
|
25
|
+
initialize(): void {
|
|
26
|
+
this.setupTabListeners()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private setupTabListeners(): void {
|
|
30
|
+
// Listen for tab open
|
|
31
|
+
this.app.tabOpened$.subscribe(tab => this.onTabOpened(tab))
|
|
32
|
+
|
|
33
|
+
// Listen for tab close - cleanup
|
|
34
|
+
this.app.tabClosed$.subscribe(tab => this.onTabClosed(tab))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private onTabOpened(tab: unknown): void {
|
|
38
|
+
if (!(tab instanceof SplitTabComponent)) return
|
|
39
|
+
|
|
40
|
+
// Small delay to let Angular finish rendering
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
const workspaceId = this.extractWorkspaceId(tab)
|
|
43
|
+
if (!workspaceId) return
|
|
44
|
+
|
|
45
|
+
const workspace = this.workspaceService.getWorkspaces()
|
|
46
|
+
.find(w => w.id === workspaceId)
|
|
47
|
+
|
|
48
|
+
if (workspace?.background && workspace.background.type !== 'none') {
|
|
49
|
+
this.applyBackground(workspaceId, workspace.background)
|
|
50
|
+
}
|
|
51
|
+
}, 200)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private onTabClosed(tab: unknown): void {
|
|
55
|
+
if (!(tab instanceof SplitTabComponent)) return
|
|
56
|
+
|
|
57
|
+
const workspaceId = this.extractWorkspaceId(tab)
|
|
58
|
+
if (workspaceId) {
|
|
59
|
+
this.removeBackground(workspaceId)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract workspace ID from a SplitTabComponent.
|
|
65
|
+
* Tries multiple strategies: _recoveredState and child profile ID.
|
|
66
|
+
*/
|
|
67
|
+
private extractWorkspaceId(tab: SplitTabComponent): string | undefined {
|
|
68
|
+
const tabAny = tab as any
|
|
69
|
+
|
|
70
|
+
// Strategy 1: Check _recoveredState.workspaceId (for restored tabs)
|
|
71
|
+
if (tabAny._recoveredState?.workspaceId) {
|
|
72
|
+
return tabAny._recoveredState.workspaceId
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Strategy 2: Extract from child profile ID (for freshly opened tabs)
|
|
76
|
+
const profilePrefix = `split-layout:${CONFIG_KEY}:`
|
|
77
|
+
for (const child of tab.getAllTabs()) {
|
|
78
|
+
const profileId = (child as any).profile?.id ?? ''
|
|
79
|
+
if (profileId.startsWith(profilePrefix)) {
|
|
80
|
+
// Profile ID format: split-layout:CONFIG_KEY:name:UUID
|
|
81
|
+
const parts = profileId.split(':')
|
|
82
|
+
return parts[parts.length - 1]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private applyBackground(workspaceId: string, bg: WorkspaceBackground): void {
|
|
90
|
+
// Mark split-tab element with data attribute
|
|
91
|
+
this.markSplitTabElement(workspaceId)
|
|
92
|
+
|
|
93
|
+
// Generate and inject CSS
|
|
94
|
+
const css = this.generateCSS(workspaceId, bg)
|
|
95
|
+
this.injectCSS(workspaceId, css)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private markSplitTabElement(workspaceId: string): void {
|
|
99
|
+
// Find split-tab that doesn't have a workspace-id yet
|
|
100
|
+
const splitTabs = document.querySelectorAll('split-tab')
|
|
101
|
+
for (let i = splitTabs.length - 1; i >= 0; i--) {
|
|
102
|
+
const splitTab = splitTabs[i]
|
|
103
|
+
if (!splitTab.hasAttribute('data-workspace-id')) {
|
|
104
|
+
splitTab.setAttribute('data-workspace-id', workspaceId)
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private generateCSS(workspaceId: string, bg: WorkspaceBackground): string {
|
|
111
|
+
if (bg.type === 'none' || !bg.value) return ''
|
|
112
|
+
|
|
113
|
+
return `
|
|
114
|
+
split-tab[data-workspace-id="${workspaceId}"] {
|
|
115
|
+
background: ${bg.value} !important;
|
|
116
|
+
}
|
|
117
|
+
split-tab[data-workspace-id="${workspaceId}"] .xterm-viewport,
|
|
118
|
+
split-tab[data-workspace-id="${workspaceId}"] .xterm-screen {
|
|
119
|
+
background: transparent !important;
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private injectCSS(workspaceId: string, css: string): void {
|
|
125
|
+
if (!this.styleElement) {
|
|
126
|
+
this.styleElement = document.createElement('style')
|
|
127
|
+
this.styleElement.id = 'tabbyspaces-backgrounds'
|
|
128
|
+
document.head.appendChild(this.styleElement)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.appliedWorkspaces.set(workspaceId, css)
|
|
132
|
+
this.updateStyleElement()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private removeBackground(workspaceId: string): void {
|
|
136
|
+
this.appliedWorkspaces.delete(workspaceId)
|
|
137
|
+
this.updateStyleElement()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private updateStyleElement(): void {
|
|
141
|
+
if (this.styleElement) {
|
|
142
|
+
this.styleElement.textContent = Array.from(this.appliedWorkspaces.values()).join('\n')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Refresh background for a specific workspace.
|
|
148
|
+
* Call this when workspace background is updated in settings.
|
|
149
|
+
*/
|
|
150
|
+
refreshWorkspaceBackground(workspaceId: string): void {
|
|
151
|
+
const workspace = this.workspaceService.getWorkspaces()
|
|
152
|
+
.find(w => w.id === workspaceId)
|
|
153
|
+
|
|
154
|
+
if (!workspace) {
|
|
155
|
+
this.removeBackground(workspaceId)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (workspace.background && workspace.background.type !== 'none') {
|
|
160
|
+
const css = this.generateCSS(workspaceId, workspace.background)
|
|
161
|
+
this.appliedWorkspaces.set(workspaceId, css)
|
|
162
|
+
} else {
|
|
163
|
+
this.appliedWorkspaces.delete(workspaceId)
|
|
164
|
+
}
|
|
165
|
+
this.updateStyleElement()
|
|
166
|
+
}
|
|
167
|
+
}
|