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,56 +1,111 @@
|
|
|
1
|
-
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
const
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
119
|
-
this.
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
107
|
-
color:
|
|
183
|
+
icon: getRandomIcon(),
|
|
184
|
+
color: getRandomColor(),
|
|
108
185
|
root: createDefaultSplit(),
|
|
109
|
-
|
|
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 = '
|
|
9
|
+
icon = IS_DEV ? 'bolt' : 'th-large'
|
|
10
10
|
title = DISPLAY_NAME
|
|
11
11
|
|
|
12
12
|
getComponentType(): any {
|