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,56 +1,111 @@
1
- import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
2
- import { ConfigService } from 'tabby-core'
1
+ import { Component, OnInit, OnDestroy, AfterViewInit, ChangeDetectorRef, ElementRef, NgZone } from '@angular/core'
2
+ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
3
+ import { ConfigService, ProfilesService } from 'tabby-core'
3
4
  import { Subscription } from 'rxjs'
5
+ import { StartupCommandService } from '../services/startupCommand.service'
4
6
  import { WorkspaceEditorService } from '../services/workspaceEditor.service'
7
+ import { DeleteConfirmModalComponent } from './deleteConfirmModal.component'
5
8
  import {
6
9
  Workspace,
7
10
  WorkspacePane,
8
11
  WorkspaceSplit,
12
+ TabbyProfile,
9
13
  countPanes,
10
14
  createDefaultWorkspace,
15
+ deepClone,
11
16
  isWorkspaceSplit,
12
17
  } from '../models/workspace.model'
13
18
 
19
+ const SETTINGS_MAX_WIDTH = '876px'
20
+
14
21
  @Component({
15
22
  selector: 'workspace-list',
16
23
  template: require('./workspaceList.component.pug'),
17
24
  styles: [require('./workspaceList.component.scss')],
18
25
  })
19
- export class WorkspaceListComponent implements OnInit, OnDestroy {
26
+ export class WorkspaceListComponent implements OnInit, OnDestroy, AfterViewInit {
20
27
  workspaces: Workspace[] = []
28
+ selectedWorkspace: Workspace | null = null
21
29
  editingWorkspace: Workspace | null = null
22
- showEditor = false
30
+ isCreatingNew = false
31
+ openingWorkspaceId: string | null = null
32
+ displayTabs: Array<{ workspace: Workspace; isNew: boolean }> = []
33
+ private cachedProfiles: TabbyProfile[] = []
23
34
  private configSubscription: Subscription | null = null
24
35
 
25
36
  constructor(
26
37
  public config: ConfigService,
27
38
  private workspaceService: WorkspaceEditorService,
28
- private cdr: ChangeDetectorRef
39
+ private profilesService: ProfilesService,
40
+ private startupService: StartupCommandService,
41
+ private modalService: NgbModal,
42
+ private cdr: ChangeDetectorRef,
43
+ private elementRef: ElementRef,
44
+ private zone: NgZone
29
45
  ) {}
30
46
 
31
- ngOnInit(): void {
47
+ async ngOnInit(): Promise<void> {
32
48
  this.loadWorkspaces()
49
+ this.autoSelectFirst()
50
+ this.cachedProfiles = await this.workspaceService.getAvailableProfiles()
33
51
  this.configSubscription = this.config.changed$.subscribe(() => {
34
- this.loadWorkspaces()
52
+ this.zone.run(() => this.loadWorkspaces())
35
53
  })
36
54
  }
37
55
 
56
+ ngAfterViewInit(): void {
57
+ // Hack: Override Tabby's settings-tab-body max-width restriction
58
+ setTimeout(() => {
59
+ const parent = this.elementRef.nativeElement.closest('settings-tab-body') as HTMLElement
60
+ if (parent) {
61
+ parent.style.maxWidth = SETTINGS_MAX_WIDTH
62
+ }
63
+ }, 0)
64
+ }
65
+
66
+ private autoSelectFirst(): void {
67
+ if (this.workspaces.length > 0 && !this.selectedWorkspace) {
68
+ this.selectWorkspace(this.workspaces[0])
69
+ }
70
+ }
71
+
72
+ selectWorkspace(workspace: Workspace): void {
73
+ this.isCreatingNew = false
74
+ this.selectedWorkspace = workspace
75
+ this.editingWorkspace = deepClone(workspace)
76
+ this.updateDisplayTabs()
77
+ }
78
+
79
+ isSelected(workspace: Workspace): boolean {
80
+ return this.selectedWorkspace?.id === workspace.id
81
+ }
82
+
38
83
  ngOnDestroy(): void {
39
84
  this.configSubscription?.unsubscribe()
40
85
  }
41
86
 
42
87
  loadWorkspaces(): void {
88
+ const previousSelectedId = this.selectedWorkspace?.id
43
89
  this.workspaces = this.workspaceService.getWorkspaces()
44
- this.cdr.detectChanges()
90
+
91
+ // Re-sync selectedWorkspace to point to object in new array
92
+ // This prevents stale reference after delete/reload operations
93
+ if (previousSelectedId) {
94
+ this.selectedWorkspace = this.workspaces.find(w => w.id === previousSelectedId) || null
95
+ }
96
+
97
+ this.updateDisplayTabs()
45
98
  }
46
99
 
47
- async createWorkspace(): Promise<void> {
48
- const profiles = await this.workspaceService.getAvailableProfiles()
49
- const defaultProfileId = profiles[0]?.id || ''
100
+ createWorkspace(): void {
101
+ const defaultProfileId = this.cachedProfiles[0]?.id || ''
50
102
  const workspace = createDefaultWorkspace()
51
103
  this.setProfileForAllPanes(workspace.root, defaultProfileId)
104
+ this.selectedWorkspace = null
52
105
  this.editingWorkspace = workspace
53
- this.showEditor = true
106
+ this.isCreatingNew = true
107
+ this.updateDisplayTabs()
108
+ this.cdr.detectChanges()
54
109
  }
55
110
 
56
111
  private setProfileForAllPanes(node: WorkspacePane | WorkspaceSplit, profileId: string): void {
@@ -62,49 +117,93 @@ export class WorkspaceListComponent implements OnInit, OnDestroy {
62
117
  }
63
118
 
64
119
  editWorkspace(workspace: Workspace): void {
65
- this.editingWorkspace = JSON.parse(JSON.stringify(workspace))
66
- this.showEditor = true
120
+ this.selectWorkspace(workspace)
67
121
  }
68
122
 
69
- async duplicateWorkspace(workspace: Workspace): Promise<void> {
123
+ async duplicateWorkspace(event: MouseEvent, workspace: Workspace): Promise<void> {
124
+ event.stopPropagation()
70
125
  const clone = this.workspaceService.duplicateWorkspace(workspace)
71
126
  await this.workspaceService.addWorkspace(clone)
72
- this.loadWorkspaces()
127
+
128
+ this.zone.run(() => {
129
+ this.loadWorkspaces()
130
+ const duplicated = this.workspaces.find((w) => w.id === clone.id)
131
+ if (duplicated) {
132
+ this.selectWorkspace(duplicated)
133
+ }
134
+ })
73
135
  }
74
136
 
75
- async deleteWorkspace(workspace: Workspace): Promise<void> {
76
- console.log('[TabbySpaces] deleteWorkspace called', workspace.id)
77
- if (confirm(`Delete workspace "${workspace.name}"?`)) {
78
- console.log('[TabbySpaces] confirm = true, calling service.deleteWorkspace')
79
- await this.workspaceService.deleteWorkspace(workspace.id)
80
- console.log('[TabbySpaces] service.deleteWorkspace done, calling loadWorkspaces')
137
+ async deleteWorkspace(event: MouseEvent, workspace: Workspace): Promise<void> {
138
+ event.stopPropagation()
139
+
140
+ const confirmed = await this.confirmDelete(workspace.name)
141
+ if (!confirmed) return
142
+
143
+ const wasSelected = this.selectedWorkspace?.id === workspace.id
144
+ const deletedIndex = this.workspaces.findIndex((w) => w.id === workspace.id)
145
+
146
+ await this.workspaceService.deleteWorkspace(workspace.id)
147
+
148
+ this.zone.run(() => {
81
149
  this.loadWorkspaces()
82
- console.log('[TabbySpaces] loadWorkspaces done, workspaces:', this.workspaces.length)
150
+ if (this.workspaces.length === 0) {
151
+ this.selectedWorkspace = null
152
+ this.editingWorkspace = null
153
+ this.isCreatingNew = false
154
+ } else if (wasSelected) {
155
+ const nextIndex = Math.min(deletedIndex, this.workspaces.length - 1)
156
+ this.selectWorkspace(this.workspaces[nextIndex])
157
+ }
158
+ })
159
+ }
160
+
161
+ private async confirmDelete(name: string): Promise<boolean> {
162
+ const modalRef = this.modalService.open(DeleteConfirmModalComponent)
163
+ modalRef.componentInstance.workspaceName = name
164
+ try {
165
+ await modalRef.result
166
+ return true
167
+ } catch {
168
+ return false
83
169
  }
84
170
  }
85
171
 
86
172
  async onEditorSave(workspace: Workspace): Promise<void> {
87
- console.log('[TabbySpaces] onEditorSave called', workspace.id, workspace.name)
88
- const existing = this.workspaces.find((w) => w.id === workspace.id)
89
- console.log('[TabbySpaces] existing workspace?', !!existing)
90
- if (existing) {
91
- await this.workspaceService.updateWorkspace(workspace)
92
- } else {
173
+ const isNew = !this.workspaces.find((w) => w.id === workspace.id)
174
+ if (isNew) {
93
175
  await this.workspaceService.addWorkspace(workspace)
176
+ } else {
177
+ await this.workspaceService.updateWorkspace(workspace)
94
178
  }
95
- console.log('[TabbySpaces] save done, calling loadWorkspaces')
96
- this.loadWorkspaces()
97
- console.log('[TabbySpaces] calling closeEditor')
98
- this.closeEditor()
99
- console.log('[TabbySpaces] closeEditor done, showEditor:', this.showEditor)
179
+
180
+ // Wrap state changes in zone.run to ensure proper change detection
181
+ this.zone.run(() => {
182
+ this.loadWorkspaces()
183
+ this.isCreatingNew = false
184
+ const saved = this.workspaces.find((w) => w.id === workspace.id)
185
+ if (saved) {
186
+ this.selectWorkspace(saved)
187
+ }
188
+ })
100
189
  }
101
190
 
102
- closeEditor(): void {
103
- console.log('[TabbySpaces] closeEditor called, showEditor before:', this.showEditor)
104
- this.showEditor = false
105
- this.editingWorkspace = null
191
+ onEditorCancel(): void {
192
+ if (this.isCreatingNew) {
193
+ // Cancel new workspace creation - go back to first workspace or empty
194
+ this.isCreatingNew = false
195
+ if (this.workspaces.length > 0) {
196
+ this.selectWorkspace(this.workspaces[0])
197
+ } else {
198
+ this.selectedWorkspace = null
199
+ this.editingWorkspace = null
200
+ this.updateDisplayTabs()
201
+ }
202
+ } else if (this.selectedWorkspace) {
203
+ // Reset to original workspace data
204
+ this.editingWorkspace = deepClone(this.selectedWorkspace)
205
+ }
106
206
  this.cdr.detectChanges()
107
- console.log('[TabbySpaces] closeEditor done, showEditor after:', this.showEditor)
108
207
  }
109
208
 
110
209
  getPaneCount(workspace: Workspace): number {
@@ -115,10 +214,48 @@ export class WorkspaceListComponent implements OnInit, OnDestroy {
115
214
  return workspace.root.orientation === 'horizontal' ? 'horizontal' : 'vertical'
116
215
  }
117
216
 
118
- async setAsDefault(workspace: Workspace): Promise<void> {
119
- this.workspaces.forEach((w) => (w.isDefault = false))
120
- workspace.isDefault = true
121
- await this.workspaceService.saveWorkspaces(this.workspaces)
122
- this.loadWorkspaces()
217
+ get hasUnsavedChanges(): boolean {
218
+ if (!this.editingWorkspace || !this.selectedWorkspace) return this.isCreatingNew
219
+ return JSON.stringify(this.editingWorkspace) !== JSON.stringify(this.selectedWorkspace)
123
220
  }
221
+
222
+ // Update display tabs array (call after state changes)
223
+ private updateDisplayTabs(): void {
224
+ const tabs = this.workspaces.map(w => ({ workspace: w, isNew: false }))
225
+ if (this.isCreatingNew && this.editingWorkspace) {
226
+ tabs.push({ workspace: this.editingWorkspace, isNew: true })
227
+ }
228
+ this.displayTabs = tabs
229
+ }
230
+
231
+ isTabSelected(tab: { workspace: Workspace; isNew: boolean }): boolean {
232
+ if (tab.isNew) return true
233
+ return this.selectedWorkspace?.id === tab.workspace.id
234
+ }
235
+
236
+ trackByTab(index: number, tab: { workspace: Workspace; isNew: boolean }): string {
237
+ return tab.isNew ? '__new__' : tab.workspace.id
238
+ }
239
+
240
+ async openWorkspace(event: MouseEvent, workspace: Workspace): Promise<void> {
241
+ event.stopPropagation()
242
+ if (this.openingWorkspaceId) return
243
+ this.openingWorkspaceId = workspace.id
244
+
245
+ try {
246
+ const commands = this.workspaceService.collectStartupCommands(workspace)
247
+ if (commands.length > 0) {
248
+ this.startupService.registerCommands(commands)
249
+ }
250
+
251
+ const profile = await this.workspaceService.generateTabbyProfile(workspace)
252
+ this.zone.run(() => {
253
+ this.profilesService.openNewTabForProfile(profile)
254
+ })
255
+ } finally {
256
+ this.openingWorkspaceId = null
257
+ this.cdr.detectChanges()
258
+ }
259
+ }
260
+
124
261
  }
package/src/index.ts CHANGED
@@ -8,11 +8,14 @@ import { WorkspaceEditorConfigProvider } from './providers/config.provider'
8
8
  import { WorkspaceEditorSettingsProvider } from './providers/settings.provider'
9
9
  import { WorkspaceToolbarProvider } from './providers/toolbar.provider'
10
10
  import { WorkspaceEditorService } from './services/workspaceEditor.service'
11
+ import { StartupCommandService } from './services/startupCommand.service'
12
+ import { WorkspaceBackgroundService } from './services/workspaceBackground.service'
11
13
 
12
14
  import { WorkspaceListComponent } from './components/workspaceList.component'
13
15
  import { WorkspaceEditorComponent } from './components/workspaceEditor.component'
14
16
  import { PaneEditorComponent } from './components/paneEditor.component'
15
17
  import { SplitPreviewComponent } from './components/splitPreview.component'
18
+ import { DeleteConfirmModalComponent } from './components/deleteConfirmModal.component'
16
19
 
17
20
  @NgModule({
18
21
  imports: [CommonModule, FormsModule],
@@ -21,12 +24,15 @@ import { SplitPreviewComponent } from './components/splitPreview.component'
21
24
  { provide: SettingsTabProvider, useClass: WorkspaceEditorSettingsProvider, multi: true },
22
25
  { provide: ToolbarButtonProvider, useClass: WorkspaceToolbarProvider, multi: true },
23
26
  WorkspaceEditorService,
27
+ StartupCommandService,
28
+ WorkspaceBackgroundService,
24
29
  ],
25
30
  declarations: [
26
31
  WorkspaceListComponent,
27
32
  WorkspaceEditorComponent,
28
33
  PaneEditorComponent,
29
34
  SplitPreviewComponent,
35
+ DeleteConfirmModalComponent,
30
36
  ],
31
37
  })
32
38
  export default class WorkspaceEditorModule {}
@@ -37,6 +37,9 @@ export interface TabbyRecoveryToken {
37
37
  tabTitle?: string
38
38
  tabCustomTitle?: string
39
39
  disableDynamicTitle?: boolean
40
+ cwd?: string
41
+ // Allow custom properties (matches Tabby's RecoveryToken interface)
42
+ [key: string]: any
40
43
  }
41
44
 
42
45
  export interface TabbySplitLayoutProfile {
@@ -58,7 +61,6 @@ export interface WorkspacePane {
58
61
  profileId: string
59
62
  cwd?: string
60
63
  startupCommand?: string
61
- title?: string
62
64
  }
63
65
 
64
66
  export interface WorkspaceSplit {
@@ -67,30 +69,71 @@ export interface WorkspaceSplit {
67
69
  children: (WorkspacePane | WorkspaceSplit)[]
68
70
  }
69
71
 
72
+ export interface WorkspaceBackground {
73
+ type: 'none' | 'solid' | 'gradient' | 'image'
74
+ value: string // CSS value: hex, gradient string, or URL
75
+ }
76
+
70
77
  export interface Workspace {
71
78
  id: string
72
79
  name: string
73
80
  icon?: string
74
81
  color?: string
82
+ background?: WorkspaceBackground
75
83
  root: WorkspaceSplit
76
- isDefault?: boolean
77
- hotkey?: string
84
+ launchOnStartup?: boolean
78
85
  }
79
86
 
87
+ // Preset backgrounds for quick selection
88
+ export const BACKGROUND_PRESETS: WorkspaceBackground[] = [
89
+ { type: 'none', value: '' },
90
+ // Existing presets
91
+ { type: 'gradient', value: 'linear-gradient(132deg, transparent 83%, rgba(6, 220, 249, 0.18) 100%), linear-gradient(210deg, transparent 85%, rgba(139, 92, 246, 0.2) 100%)' },
92
+ { type: 'gradient', value: 'linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, transparent 50%)' },
93
+ { type: 'gradient', value: 'linear-gradient(45deg, rgba(239, 68, 68, 0.1) 0%, transparent 50%)' },
94
+ { type: 'gradient', value: 'linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, transparent 50%)' },
95
+ { type: 'gradient', value: 'linear-gradient(225deg, transparent 70%, rgba(249, 115, 22, 0.15) 100%)' },
96
+ { type: 'gradient', value: 'linear-gradient(180deg, rgba(139, 92, 246, 0.1) 0%, transparent 40%)' },
97
+ // New presets
98
+ { type: 'gradient', value: 'linear-gradient(315deg, transparent 80%, rgba(236, 72, 153, 0.15) 100%)' }, // Pink bottom-right
99
+ { type: 'gradient', value: 'linear-gradient(0deg, rgba(6, 182, 212, 0.12) 0%, transparent 35%)' }, // Cyan bottom
100
+ { type: 'gradient', value: 'linear-gradient(45deg, transparent 85%, rgba(234, 179, 8, 0.18) 100%), linear-gradient(225deg, transparent 85%, rgba(249, 115, 22, 0.15) 100%)' }, // Gold corners
101
+ { type: 'gradient', value: 'linear-gradient(160deg, rgba(34, 197, 94, 0.12) 0%, transparent 40%)' }, // Green top-left
102
+ { type: 'gradient', value: 'linear-gradient(200deg, transparent 75%, rgba(99, 102, 241, 0.18) 100%)' }, // Indigo bottom-left
103
+ { type: 'gradient', value: 'linear-gradient(135deg, rgba(20, 184, 166, 0.1) 0%, transparent 50%), linear-gradient(315deg, rgba(139, 92, 246, 0.1) 0%, transparent 50%)' }, // Teal + Violet diagonal
104
+ { type: 'gradient', value: 'linear-gradient(90deg, rgba(239, 68, 68, 0.08) 0%, transparent 30%, transparent 70%, rgba(59, 130, 246, 0.08) 100%)' }, // Red-Blue sides
105
+ { type: 'gradient', value: 'linear-gradient(180deg, transparent 60%, rgba(16, 185, 129, 0.12) 100%)' }, // Emerald bottom fade
106
+ { type: 'gradient', value: 'linear-gradient(45deg, rgba(168, 85, 247, 0.1) 0%, transparent 40%), linear-gradient(225deg, rgba(6, 182, 212, 0.1) 0%, transparent 40%)' }, // Purple + Cyan corners
107
+ { type: 'gradient', value: 'linear-gradient(150deg, transparent 70%, rgba(251, 146, 60, 0.15) 100%), linear-gradient(30deg, transparent 70%, rgba(251, 146, 60, 0.1) 100%)' }, // Warm orange accents
108
+ ]
109
+
110
+ /**
111
+ * Type guard to check if a node is a WorkspaceSplit.
112
+ * @param node - The node to check
113
+ * @returns True if the node is a WorkspaceSplit
114
+ */
80
115
  export function isWorkspaceSplit(node: WorkspacePane | WorkspaceSplit): node is WorkspaceSplit {
81
116
  return 'orientation' in node && 'children' in node
82
117
  }
83
118
 
119
+ /**
120
+ * Creates a new pane with default configuration.
121
+ * @returns A new WorkspacePane with generated UUID and empty settings
122
+ */
84
123
  export function createDefaultPane(): WorkspacePane {
85
124
  return {
86
125
  id: generateUUID(),
87
126
  profileId: '',
88
127
  cwd: '',
89
128
  startupCommand: '',
90
- title: '',
91
129
  }
92
130
  }
93
131
 
132
+ /**
133
+ * Creates a new split with two default panes.
134
+ * @param orientation - Split direction ('horizontal' or 'vertical'), defaults to 'horizontal'
135
+ * @returns A new WorkspaceSplit with two panes at 50/50 ratio
136
+ */
94
137
  export function createDefaultSplit(orientation: 'horizontal' | 'vertical' = 'horizontal'): WorkspaceSplit {
95
138
  return {
96
139
  orientation,
@@ -99,17 +142,52 @@ export function createDefaultSplit(orientation: 'horizontal' | 'vertical' = 'hor
99
142
  }
100
143
  }
101
144
 
102
- export function createDefaultWorkspace(name: string = 'New Workspace'): Workspace {
145
+ // Color palette for workspaces
146
+ const WORKSPACE_COLORS = [
147
+ '#3b82f6', // blue
148
+ '#10b981', // emerald
149
+ '#f59e0b', // amber
150
+ '#ef4444', // red
151
+ '#8b5cf6', // violet
152
+ '#ec4899', // pink
153
+ '#06b6d4', // cyan
154
+ '#f97316', // orange
155
+ ]
156
+
157
+ // Icon list for workspaces
158
+ const WORKSPACE_ICONS = [
159
+ 'columns', 'terminal', 'code', 'folder', 'home', 'briefcase',
160
+ 'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
161
+ 'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
162
+ ]
163
+
164
+ /** Returns a random color from the workspace color palette. */
165
+ export function getRandomColor(): string {
166
+ return WORKSPACE_COLORS[Math.floor(Math.random() * WORKSPACE_COLORS.length)]
167
+ }
168
+
169
+ /** Returns a random icon from the workspace icon set. */
170
+ export function getRandomIcon(): string {
171
+ return WORKSPACE_ICONS[Math.floor(Math.random() * WORKSPACE_ICONS.length)]
172
+ }
173
+
174
+ /**
175
+ * Creates a new workspace with default configuration.
176
+ * @param name - Display name for the workspace (optional)
177
+ * @returns A new Workspace with generated UUID, random icon/color, and a default split
178
+ */
179
+ export function createDefaultWorkspace(name: string = ''): Workspace {
103
180
  return {
104
181
  id: generateUUID(),
105
182
  name,
106
- icon: 'columns',
107
- color: '#3b82f6',
183
+ icon: getRandomIcon(),
184
+ color: getRandomColor(),
108
185
  root: createDefaultSplit(),
109
- isDefault: false,
186
+ launchOnStartup: false,
110
187
  }
111
188
  }
112
189
 
190
+ /** Generates a random UUID v4 string. */
113
191
  export function generateUUID(): string {
114
192
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
115
193
  const r = (Math.random() * 16) | 0
@@ -118,9 +196,36 @@ export function generateUUID(): string {
118
196
  })
119
197
  }
120
198
 
199
+ /**
200
+ * Recursively counts the total number of panes in a split tree.
201
+ * @param node - The root node to count from
202
+ * @returns Total number of panes in the tree
203
+ */
121
204
  export function countPanes(node: WorkspacePane | WorkspaceSplit): number {
122
205
  if (isWorkspaceSplit(node)) {
123
206
  return node.children.reduce((sum, child) => sum + countPanes(child), 0)
124
207
  }
125
208
  return 1
126
209
  }
210
+
211
+ /**
212
+ * Creates a deep clone of an object, preserving type information.
213
+ * More efficient than JSON.parse(JSON.stringify()) for simple objects.
214
+ * @param obj - The object to clone
215
+ * @returns A deep copy of the object
216
+ */
217
+ export function deepClone<T>(obj: T): T {
218
+ if (obj === null || typeof obj !== 'object') {
219
+ return obj
220
+ }
221
+ if (Array.isArray(obj)) {
222
+ return obj.map(item => deepClone(item)) as T
223
+ }
224
+ const cloned = {} as T
225
+ for (const key in obj) {
226
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
227
+ cloned[key] = deepClone(obj[key])
228
+ }
229
+ }
230
+ return cloned
231
+ }
@@ -1,12 +1,12 @@
1
1
  import { Injectable } from '@angular/core'
2
2
  import { SettingsTabProvider } from 'tabby-settings'
3
3
  import { WorkspaceListComponent } from '../components/workspaceList.component'
4
- import { CONFIG_KEY, DISPLAY_NAME } from '../build-config'
4
+ import { CONFIG_KEY, DISPLAY_NAME, IS_DEV } from '../build-config'
5
5
 
6
6
  @Injectable()
7
7
  export class WorkspaceEditorSettingsProvider extends SettingsTabProvider {
8
8
  id = CONFIG_KEY
9
- icon = 'columns'
9
+ icon = IS_DEV ? 'bolt' : 'th-large'
10
10
  title = DISPLAY_NAME
11
11
 
12
12
  getComponentType(): any {