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.
Files changed (78) hide show
  1. package/.claude/settings.local.json +29 -2
  2. package/.github/workflows/ci.yml +26 -0
  3. package/.github/workflows/claude-code-review.yml +44 -0
  4. package/.github/workflows/claude.yml +81 -0
  5. package/.github/workflows/release.yml +30 -0
  6. package/CHANGELOG.md +92 -20
  7. package/CLAUDE.md +196 -15
  8. package/CONTRIBUTING.md +3 -1
  9. package/README.md +80 -61
  10. package/RELEASE.md +91 -0
  11. package/TODO.md +77 -0
  12. package/dist/build-config.d.ts +3 -3
  13. package/dist/components/deleteConfirmModal.component.d.ts +7 -0
  14. package/dist/components/deleteConfirmModal.component.d.ts.map +1 -0
  15. package/dist/components/paneEditor.component.d.ts +9 -13
  16. package/dist/components/paneEditor.component.d.ts.map +1 -1
  17. package/dist/components/splitPreview.component.d.ts +50 -35
  18. package/dist/components/splitPreview.component.d.ts.map +1 -1
  19. package/dist/components/workspaceEditor.component.d.ts +61 -28
  20. package/dist/components/workspaceEditor.component.d.ts.map +1 -1
  21. package/dist/components/workspaceList.component.d.ts +56 -27
  22. package/dist/components/workspaceList.component.d.ts.map +1 -1
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.LICENSE.txt +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/models/workspace.model.d.ts +118 -76
  29. package/dist/models/workspace.model.d.ts.map +1 -1
  30. package/dist/package.json +26 -0
  31. package/dist/providers/config.provider.d.ts +8 -8
  32. package/dist/providers/settings.provider.d.ts +7 -7
  33. package/dist/providers/settings.provider.d.ts.map +1 -1
  34. package/dist/providers/toolbar.provider.d.ts +23 -12
  35. package/dist/providers/toolbar.provider.d.ts.map +1 -1
  36. package/dist/services/startupCommand.service.d.ts +28 -0
  37. package/dist/services/startupCommand.service.d.ts.map +1 -0
  38. package/dist/services/workspaceBackground.service.d.ts +38 -0
  39. package/dist/services/workspaceBackground.service.d.ts.map +1 -0
  40. package/dist/services/workspaceEditor.service.d.ts +46 -24
  41. package/dist/services/workspaceEditor.service.d.ts.map +1 -1
  42. package/docs/DESIGN.md +57 -0
  43. package/docs/SESSION-2026-01-14-S1-DESIGN.md +134 -0
  44. package/docs/marketing_status.md +92 -0
  45. package/mockups/index.html +162 -0
  46. package/mockups/s1-tight-sharp.html +522 -0
  47. package/mockups/shared/base.css +216 -0
  48. package/mockups/v06-tabbed.html +643 -0
  49. package/package.json +3 -7
  50. package/screenshots/editor.png +0 -0
  51. package/screenshots/pane-edit.png +0 -0
  52. package/scripts/build-dev.js +2 -1
  53. package/scripts/build-prod.js +40 -0
  54. package/src/components/deleteConfirmModal.component.ts +23 -0
  55. package/src/components/paneEditor.component.pug +27 -43
  56. package/src/components/paneEditor.component.scss +37 -85
  57. package/src/components/paneEditor.component.ts +6 -16
  58. package/src/components/splitPreview.component.pug +36 -5
  59. package/src/components/splitPreview.component.scss +78 -45
  60. package/src/components/splitPreview.component.ts +83 -18
  61. package/src/components/workspaceEditor.component.pug +162 -74
  62. package/src/components/workspaceEditor.component.scss +261 -108
  63. package/src/components/workspaceEditor.component.ts +294 -31
  64. package/src/components/workspaceList.component.pug +32 -41
  65. package/src/components/workspaceList.component.scss +89 -74
  66. package/src/components/workspaceList.component.ts +181 -44
  67. package/src/index.ts +6 -0
  68. package/src/models/workspace.model.ts +113 -8
  69. package/src/providers/settings.provider.ts +2 -2
  70. package/src/providers/toolbar.provider.ts +113 -13
  71. package/src/services/startupCommand.service.ts +140 -0
  72. package/src/services/workspaceBackground.service.ts +167 -0
  73. package/src/services/workspaceEditor.service.ts +134 -65
  74. package/src/styles/_index.scss +3 -0
  75. package/src/styles/_mixins.scss +180 -0
  76. package/src/styles/_variables.scss +67 -0
  77. package/RELEASE_PLAN.md +0 -161
  78. 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: `<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">
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: '__settings__'
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 === '__settings__') {
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
- const profile = this.workspaceService.generateTabbyProfile(workspace)
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
+ }