tabby-tabbyspaces 0.0.1

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 (61) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. package/CHANGELOG.md +20 -0
  6. package/CLAUDE.md +159 -0
  7. package/CONTRIBUTING.md +64 -0
  8. package/LICENSE +21 -0
  9. package/README.md +61 -0
  10. package/RELEASE_PLAN.md +161 -0
  11. package/dist/build-config.d.ts +4 -0
  12. package/dist/build-config.d.ts.map +1 -0
  13. package/dist/components/paneEditor.component.d.ts +14 -0
  14. package/dist/components/paneEditor.component.d.ts.map +1 -0
  15. package/dist/components/splitPreview.component.d.ts +36 -0
  16. package/dist/components/splitPreview.component.d.ts.map +1 -0
  17. package/dist/components/workspaceEditor.component.d.ts +29 -0
  18. package/dist/components/workspaceEditor.component.d.ts.map +1 -0
  19. package/dist/components/workspaceList.component.d.ts +28 -0
  20. package/dist/components/workspaceList.component.d.ts.map +1 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +3 -0
  24. package/dist/index.js.LICENSE.txt +43 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/models/workspace.model.d.ts +77 -0
  27. package/dist/models/workspace.model.d.ts.map +1 -0
  28. package/dist/providers/config.provider.d.ts +9 -0
  29. package/dist/providers/config.provider.d.ts.map +1 -0
  30. package/dist/providers/settings.provider.d.ts +8 -0
  31. package/dist/providers/settings.provider.d.ts.map +1 -0
  32. package/dist/providers/toolbar.provider.d.ts +13 -0
  33. package/dist/providers/toolbar.provider.d.ts.map +1 -0
  34. package/dist/services/workspaceEditor.service.d.ts +25 -0
  35. package/dist/services/workspaceEditor.service.d.ts.map +1 -0
  36. package/package.json +73 -0
  37. package/screenshots/editor.png +0 -0
  38. package/screenshots/pane-edit.png +0 -0
  39. package/screenshots/workspace-edit.png +0 -0
  40. package/scripts/build-dev.js +46 -0
  41. package/src/build-config.ts +8 -0
  42. package/src/components/paneEditor.component.pug +46 -0
  43. package/src/components/paneEditor.component.scss +112 -0
  44. package/src/components/paneEditor.component.ts +33 -0
  45. package/src/components/splitPreview.component.pug +45 -0
  46. package/src/components/splitPreview.component.scss +126 -0
  47. package/src/components/splitPreview.component.ts +111 -0
  48. package/src/components/workspaceEditor.component.pug +84 -0
  49. package/src/components/workspaceEditor.component.scss +169 -0
  50. package/src/components/workspaceEditor.component.ts +181 -0
  51. package/src/components/workspaceList.component.pug +46 -0
  52. package/src/components/workspaceList.component.scss +112 -0
  53. package/src/components/workspaceList.component.ts +124 -0
  54. package/src/index.ts +38 -0
  55. package/src/models/workspace.model.ts +126 -0
  56. package/src/providers/config.provider.ts +12 -0
  57. package/src/providers/settings.provider.ts +15 -0
  58. package/src/providers/toolbar.provider.ts +81 -0
  59. package/src/services/workspaceEditor.service.ts +228 -0
  60. package/tsconfig.json +29 -0
  61. package/webpack.config.js +62 -0
@@ -0,0 +1,84 @@
1
+ .workspace-editor-overlay((click)='onCancel()')
2
+ .workspace-editor-modal((click)='$event.stopPropagation()')
3
+ .modal-header
4
+ h4 {{ workspace.id ? 'Edit' : 'New' }} Workspace
5
+ button.btn.btn-link.close-btn(type='button', (click)='onCancel()')
6
+ i.fas.fa-times
7
+
8
+ .modal-body
9
+ .form-row
10
+ .form-group.flex-grow
11
+ label Name
12
+ input.form-control(
13
+ type='text',
14
+ [(ngModel)]='workspace.name',
15
+ placeholder='Workspace name'
16
+ )
17
+
18
+ .form-row
19
+ .form-group
20
+ label Icon
21
+ .icon-selector
22
+ button.icon-option(
23
+ *ngFor='let icon of availableIcons',
24
+ type='button',
25
+ [class.selected]='workspace.icon === icon',
26
+ (click)='workspace.icon = icon'
27
+ )
28
+ i.fas([class]='"fa-" + icon')
29
+
30
+ .form-group
31
+ label Color
32
+ input.form-control.color-input(
33
+ type='color',
34
+ [(ngModel)]='workspace.color'
35
+ )
36
+
37
+ .form-row
38
+ .form-group
39
+ label
40
+ input(type='checkbox', [(ngModel)]='workspace.isDefault')
41
+ | Launch on startup
42
+
43
+ .form-section
44
+ .section-header
45
+ h5 Layout Preview
46
+ .orientation-toggle
47
+ button.btn.btn-sm(
48
+ type='button',
49
+ [class.btn-primary]='workspace.root.orientation === "horizontal"',
50
+ [class.btn-outline-secondary]='workspace.root.orientation !== "horizontal"',
51
+ (click)='setOrientation("horizontal")'
52
+ )
53
+ i.fas.fa-arrows-alt-h
54
+ | Horizontal
55
+ button.btn.btn-sm(
56
+ type='button',
57
+ [class.btn-primary]='workspace.root.orientation === "vertical"',
58
+ [class.btn-outline-secondary]='workspace.root.orientation !== "vertical"',
59
+ (click)='setOrientation("vertical")'
60
+ )
61
+ i.fas.fa-arrows-alt-v
62
+ | Vertical
63
+
64
+ split-preview(
65
+ [split]='workspace.root',
66
+ (paneClick)='selectPane($event)',
67
+ (splitHorizontal)='splitPane($event, "horizontal")',
68
+ (splitVertical)='splitPane($event, "vertical")',
69
+ (removePane)='removePane($event)'
70
+ )
71
+
72
+ .modal-footer
73
+ button.btn.btn-secondary(type='button', (click)='onCancel()') Cancel
74
+ button.btn.btn-primary(type='button', (click)='onSave()', [disabled]='!workspace.name?.trim()')
75
+ i.fas.fa-save
76
+ | Save Workspace
77
+
78
+ pane-editor(
79
+ *ngIf='showPaneEditor && selectedPane',
80
+ [pane]='selectedPane',
81
+ [profiles]='profiles',
82
+ (save)='onPaneSave($event)',
83
+ (cancel)='closePaneEditor()'
84
+ )
@@ -0,0 +1,169 @@
1
+ .workspace-editor-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: rgba(0, 0, 0, 0.6);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ }
13
+
14
+ .workspace-editor-modal {
15
+ background: var(--theme-bg);
16
+ border-radius: 12px;
17
+ width: 90%;
18
+ max-width: 700px;
19
+ max-height: 90vh;
20
+ overflow: hidden;
21
+ display: flex;
22
+ flex-direction: column;
23
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
24
+ }
25
+
26
+ .modal-header {
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ padding: 16px 20px;
31
+ border-bottom: 1px solid var(--theme-border);
32
+
33
+ h4 {
34
+ margin: 0;
35
+ font-size: 1.25rem;
36
+ }
37
+
38
+ .close-btn {
39
+ font-size: 1.25rem;
40
+ color: var(--theme-fg-more);
41
+ padding: 4px 8px;
42
+
43
+ &:hover {
44
+ color: var(--theme-fg);
45
+ }
46
+ }
47
+ }
48
+
49
+ .modal-body {
50
+ padding: 20px;
51
+ overflow-y: auto;
52
+ flex: 1;
53
+ }
54
+
55
+ .form-row {
56
+ display: flex;
57
+ gap: 16px;
58
+ margin-bottom: 16px;
59
+
60
+ &:last-child {
61
+ margin-bottom: 0;
62
+ }
63
+ }
64
+
65
+ .form-group {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 6px;
69
+
70
+ &.flex-grow {
71
+ flex: 1;
72
+ }
73
+
74
+ label {
75
+ font-size: 0.9rem;
76
+ color: var(--theme-fg-more);
77
+ }
78
+
79
+ input[type='checkbox'] {
80
+ margin-right: 8px;
81
+ }
82
+ }
83
+
84
+ .form-control {
85
+ padding: 8px 12px;
86
+ border-radius: 6px;
87
+ border: 1px solid var(--theme-border);
88
+ background: var(--theme-bg-more);
89
+ color: var(--theme-fg);
90
+ font-size: 0.95rem;
91
+
92
+ &:focus {
93
+ outline: none;
94
+ border-color: var(--theme-primary);
95
+ }
96
+ }
97
+
98
+ .color-input {
99
+ width: 60px;
100
+ height: 36px;
101
+ padding: 2px;
102
+ cursor: pointer;
103
+ }
104
+
105
+ .icon-selector {
106
+ display: flex;
107
+ flex-wrap: wrap;
108
+ gap: 4px;
109
+ }
110
+
111
+ .icon-option {
112
+ width: 36px;
113
+ height: 36px;
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ border: 1px solid var(--theme-border);
118
+ border-radius: 6px;
119
+ background: var(--theme-bg-more);
120
+ color: var(--theme-fg-more);
121
+ cursor: pointer;
122
+ transition: all 0.2s;
123
+
124
+ &:hover {
125
+ background: var(--theme-bg-more-more);
126
+ color: var(--theme-fg);
127
+ }
128
+
129
+ &.selected {
130
+ border-color: var(--theme-primary);
131
+ background: var(--theme-primary);
132
+ color: white;
133
+ }
134
+ }
135
+
136
+ .form-section {
137
+ margin-top: 20px;
138
+
139
+ .section-header {
140
+ display: flex;
141
+ justify-content: space-between;
142
+ align-items: center;
143
+ margin-bottom: 12px;
144
+
145
+ h5 {
146
+ margin: 0;
147
+ font-size: 1rem;
148
+ }
149
+ }
150
+ }
151
+
152
+ .orientation-toggle {
153
+ display: flex;
154
+ gap: 8px;
155
+
156
+ .btn {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 6px;
160
+ }
161
+ }
162
+
163
+ .modal-footer {
164
+ display: flex;
165
+ justify-content: flex-end;
166
+ gap: 12px;
167
+ padding: 16px 20px;
168
+ border-top: 1px solid var(--theme-border);
169
+ }
@@ -0,0 +1,181 @@
1
+ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'
2
+ import {
3
+ Workspace,
4
+ WorkspacePane,
5
+ WorkspaceSplit,
6
+ TabbyProfile,
7
+ isWorkspaceSplit,
8
+ createDefaultPane,
9
+ generateUUID,
10
+ } from '../models/workspace.model'
11
+ import { WorkspaceEditorService } from '../services/workspaceEditor.service'
12
+
13
+ @Component({
14
+ selector: 'workspace-editor',
15
+ template: require('./workspaceEditor.component.pug'),
16
+ styles: [require('./workspaceEditor.component.scss')],
17
+ })
18
+ export class WorkspaceEditorComponent implements OnInit {
19
+ @Input() workspace!: Workspace
20
+ @Output() save = new EventEmitter<Workspace>()
21
+ @Output() cancel = new EventEmitter<void>()
22
+
23
+ selectedPane: WorkspacePane | null = null
24
+ showPaneEditor = false
25
+ profiles: TabbyProfile[] = []
26
+ availableIcons = [
27
+ 'columns', 'terminal', 'code', 'folder', 'home', 'briefcase',
28
+ 'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
29
+ 'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
30
+ ]
31
+
32
+ constructor(private workspaceService: WorkspaceEditorService) {}
33
+
34
+ async ngOnInit(): Promise<void> {
35
+ this.profiles = await this.workspaceService.getAvailableProfiles()
36
+ if (!this.workspace.root) {
37
+ this.workspace.root = {
38
+ orientation: 'horizontal',
39
+ ratios: [0.5, 0.5],
40
+ children: [createDefaultPane(), createDefaultPane()],
41
+ }
42
+ }
43
+ }
44
+
45
+ onSave(): void {
46
+ if (!this.workspace.name?.trim()) {
47
+ return
48
+ }
49
+ this.save.emit(this.workspace)
50
+ }
51
+
52
+ onCancel(): void {
53
+ this.cancel.emit()
54
+ }
55
+
56
+ selectPane(pane: WorkspacePane): void {
57
+ this.selectedPane = pane
58
+ this.showPaneEditor = true
59
+ }
60
+
61
+ closePaneEditor(): void {
62
+ this.showPaneEditor = false
63
+ this.selectedPane = null
64
+ }
65
+
66
+ onPaneSave(pane: WorkspacePane): void {
67
+ this.updatePaneInTree(this.workspace.root, pane)
68
+ this.closePaneEditor()
69
+ }
70
+
71
+ private updatePaneInTree(node: WorkspaceSplit, updatedPane: WorkspacePane): boolean {
72
+ for (let i = 0; i < node.children.length; i++) {
73
+ const child = node.children[i]
74
+ if (isWorkspaceSplit(child)) {
75
+ if (this.updatePaneInTree(child, updatedPane)) {
76
+ return true
77
+ }
78
+ } else if (child.id === updatedPane.id) {
79
+ node.children[i] = updatedPane
80
+ return true
81
+ }
82
+ }
83
+ return false
84
+ }
85
+
86
+ splitPane(pane: WorkspacePane, orientation: 'horizontal' | 'vertical'): void {
87
+ this.splitPaneInTree(this.workspace.root, pane, orientation)
88
+ }
89
+
90
+ private splitPaneInTree(
91
+ node: WorkspaceSplit,
92
+ targetPane: WorkspacePane,
93
+ orientation: 'horizontal' | 'vertical'
94
+ ): boolean {
95
+ for (let i = 0; i < node.children.length; i++) {
96
+ const child = node.children[i]
97
+ if (isWorkspaceSplit(child)) {
98
+ if (this.splitPaneInTree(child, targetPane, orientation)) {
99
+ return true
100
+ }
101
+ } else if (child.id === targetPane.id) {
102
+ const newPane = createDefaultPane()
103
+ newPane.profileId = child.profileId // Copy profile from source pane
104
+ const newSplit: WorkspaceSplit = {
105
+ orientation,
106
+ ratios: [0.5, 0.5],
107
+ children: [child, newPane],
108
+ }
109
+ node.children[i] = newSplit
110
+ this.recalculateRatios(node)
111
+ return true
112
+ }
113
+ }
114
+ return false
115
+ }
116
+
117
+ removePane(pane: WorkspacePane): void {
118
+ this.removePaneFromTree(this.workspace.root, pane)
119
+ }
120
+
121
+ private removePaneFromTree(node: WorkspaceSplit, targetPane: WorkspacePane): boolean {
122
+ for (let i = 0; i < node.children.length; i++) {
123
+ const child = node.children[i]
124
+ if (isWorkspaceSplit(child)) {
125
+ // Check if the pane is directly in this split
126
+ const paneIndex = child.children.findIndex(
127
+ (c) => !isWorkspaceSplit(c) && (c as WorkspacePane).id === targetPane.id
128
+ )
129
+ if (paneIndex !== -1 && child.children.length > 1) {
130
+ child.children.splice(paneIndex, 1)
131
+ this.recalculateRatios(child)
132
+ // If only one child remains, flatten
133
+ if (child.children.length === 1) {
134
+ node.children[i] = child.children[0]
135
+ }
136
+ return true
137
+ }
138
+ if (this.removePaneFromTree(child, targetPane)) {
139
+ return true
140
+ }
141
+ } else if (child.id === targetPane.id) {
142
+ if (node.children.length > 1) {
143
+ node.children.splice(i, 1)
144
+ this.recalculateRatios(node)
145
+ return true
146
+ }
147
+ }
148
+ }
149
+ return false
150
+ }
151
+
152
+ private recalculateRatios(split: WorkspaceSplit): void {
153
+ const count = split.children.length
154
+ split.ratios = split.children.map(() => 1 / count)
155
+ }
156
+
157
+ setOrientation(orientation: 'horizontal' | 'vertical'): void {
158
+ this.workspace.root.orientation = orientation
159
+ }
160
+
161
+ updateRatio(index: number, value: number): void {
162
+ const ratios = [...this.workspace.root.ratios]
163
+ const diff = value - ratios[index]
164
+
165
+ if (index < ratios.length - 1) {
166
+ ratios[index] = value
167
+ ratios[index + 1] -= diff
168
+ } else {
169
+ ratios[index] = value
170
+ ratios[index - 1] -= diff
171
+ }
172
+
173
+ // Clamp values
174
+ ratios.forEach((r, i) => {
175
+ ratios[i] = Math.max(0.1, Math.min(0.9, r))
176
+ })
177
+
178
+ this.workspace.root.ratios = ratios
179
+ }
180
+
181
+ }
@@ -0,0 +1,46 @@
1
+ .workspace-list-container
2
+ .workspace-list-header
3
+ h3 Workspace Editor
4
+ button.btn.btn-primary(type='button', (click)='createWorkspace()')
5
+ i.fas.fa-plus
6
+ | New Workspace
7
+
8
+ .workspace-list(*ngIf='workspaces.length > 0')
9
+ .workspace-item(*ngFor='let workspace of workspaces')
10
+ .workspace-info
11
+ .workspace-icon([style.color]='workspace.color')
12
+ i.fas([class]='"fa-" + (workspace.icon || "columns")')
13
+ .workspace-details
14
+ .workspace-name {{ workspace.name }}
15
+ .workspace-meta
16
+ span {{ getPaneCount(workspace) }} panes
17
+ span.separator &middot;
18
+ span {{ getOrientationLabel(workspace) }}
19
+ span.separator(*ngIf='workspace.isDefault') &middot;
20
+ span.badge.badge-primary(*ngIf='workspace.isDefault') default
21
+
22
+ .workspace-actions
23
+ button.btn.btn-link(
24
+ type='button',
25
+ title='Set as default',
26
+ (click)='setAsDefault(workspace)',
27
+ [class.active]='workspace.isDefault'
28
+ )
29
+ i.fas.fa-star
30
+ button.btn.btn-link(type='button', title='Duplicate', (click)='duplicateWorkspace(workspace)')
31
+ i.fas.fa-copy
32
+ button.btn.btn-link(type='button', title='Edit', (click)='editWorkspace(workspace)')
33
+ i.fas.fa-edit
34
+ button.btn.btn-link.text-danger(type='button', title='Delete', (click)='deleteWorkspace(workspace)')
35
+ i.fas.fa-trash
36
+
37
+ .workspace-empty(*ngIf='workspaces.length === 0')
38
+ p No workspaces configured yet.
39
+ p Click "New Workspace" to create your first split-layout workspace.
40
+
41
+ workspace-editor(
42
+ *ngIf='showEditor',
43
+ [workspace]='editingWorkspace',
44
+ (save)='onEditorSave($event)',
45
+ (cancel)='closeEditor()'
46
+ )
@@ -0,0 +1,112 @@
1
+ .workspace-list-container {
2
+ padding: 20px;
3
+ max-width: 800px;
4
+ }
5
+
6
+ .workspace-list-header {
7
+ display: flex;
8
+ justify-content: space-between;
9
+ align-items: center;
10
+ margin-bottom: 20px;
11
+
12
+ h3 {
13
+ margin: 0;
14
+ font-size: 1.5rem;
15
+ }
16
+ }
17
+
18
+ .workspace-list {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 12px;
22
+ }
23
+
24
+ .workspace-item {
25
+ display: flex;
26
+ justify-content: space-between;
27
+ align-items: center;
28
+ padding: 16px;
29
+ background: var(--theme-bg-more);
30
+ border-radius: 8px;
31
+ border: 1px solid var(--theme-border);
32
+ transition: background 0.2s;
33
+
34
+ &:hover {
35
+ background: var(--theme-bg-more-more);
36
+ }
37
+ }
38
+
39
+ .workspace-info {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 16px;
43
+ }
44
+
45
+ .workspace-icon {
46
+ font-size: 24px;
47
+ width: 40px;
48
+ text-align: center;
49
+ }
50
+
51
+ .workspace-details {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 4px;
55
+ }
56
+
57
+ .workspace-name {
58
+ font-size: 1.1rem;
59
+ font-weight: 500;
60
+ }
61
+
62
+ .workspace-meta {
63
+ font-size: 0.85rem;
64
+ color: var(--theme-fg-more);
65
+
66
+ .separator {
67
+ margin: 0 6px;
68
+ }
69
+
70
+ .badge {
71
+ font-size: 0.75rem;
72
+ padding: 2px 6px;
73
+ border-radius: 4px;
74
+ background: var(--theme-primary);
75
+ color: white;
76
+ }
77
+ }
78
+
79
+ .workspace-actions {
80
+ display: flex;
81
+ gap: 4px;
82
+
83
+ .btn-link {
84
+ padding: 8px;
85
+ color: var(--theme-fg-more);
86
+ opacity: 0.7;
87
+ transition: opacity 0.2s;
88
+
89
+ &:hover {
90
+ opacity: 1;
91
+ }
92
+
93
+ &.active {
94
+ color: gold;
95
+ opacity: 1;
96
+ }
97
+
98
+ &.text-danger:hover {
99
+ color: var(--theme-danger);
100
+ }
101
+ }
102
+ }
103
+
104
+ .workspace-empty {
105
+ text-align: center;
106
+ padding: 40px;
107
+ color: var(--theme-fg-more);
108
+
109
+ p {
110
+ margin: 8px 0;
111
+ }
112
+ }
@@ -0,0 +1,124 @@
1
+ import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
2
+ import { ConfigService } from 'tabby-core'
3
+ import { Subscription } from 'rxjs'
4
+ import { WorkspaceEditorService } from '../services/workspaceEditor.service'
5
+ import {
6
+ Workspace,
7
+ WorkspacePane,
8
+ WorkspaceSplit,
9
+ countPanes,
10
+ createDefaultWorkspace,
11
+ isWorkspaceSplit,
12
+ } from '../models/workspace.model'
13
+
14
+ @Component({
15
+ selector: 'workspace-list',
16
+ template: require('./workspaceList.component.pug'),
17
+ styles: [require('./workspaceList.component.scss')],
18
+ })
19
+ export class WorkspaceListComponent implements OnInit, OnDestroy {
20
+ workspaces: Workspace[] = []
21
+ editingWorkspace: Workspace | null = null
22
+ showEditor = false
23
+ private configSubscription: Subscription | null = null
24
+
25
+ constructor(
26
+ public config: ConfigService,
27
+ private workspaceService: WorkspaceEditorService,
28
+ private cdr: ChangeDetectorRef
29
+ ) {}
30
+
31
+ ngOnInit(): void {
32
+ this.loadWorkspaces()
33
+ this.configSubscription = this.config.changed$.subscribe(() => {
34
+ this.loadWorkspaces()
35
+ })
36
+ }
37
+
38
+ ngOnDestroy(): void {
39
+ this.configSubscription?.unsubscribe()
40
+ }
41
+
42
+ loadWorkspaces(): void {
43
+ this.workspaces = this.workspaceService.getWorkspaces()
44
+ this.cdr.detectChanges()
45
+ }
46
+
47
+ async createWorkspace(): Promise<void> {
48
+ const profiles = await this.workspaceService.getAvailableProfiles()
49
+ const defaultProfileId = profiles[0]?.id || ''
50
+ const workspace = createDefaultWorkspace()
51
+ this.setProfileForAllPanes(workspace.root, defaultProfileId)
52
+ this.editingWorkspace = workspace
53
+ this.showEditor = true
54
+ }
55
+
56
+ private setProfileForAllPanes(node: WorkspacePane | WorkspaceSplit, profileId: string): void {
57
+ if (isWorkspaceSplit(node)) {
58
+ node.children.forEach((child) => this.setProfileForAllPanes(child, profileId))
59
+ } else {
60
+ node.profileId = profileId
61
+ }
62
+ }
63
+
64
+ editWorkspace(workspace: Workspace): void {
65
+ this.editingWorkspace = JSON.parse(JSON.stringify(workspace))
66
+ this.showEditor = true
67
+ }
68
+
69
+ async duplicateWorkspace(workspace: Workspace): Promise<void> {
70
+ const clone = this.workspaceService.duplicateWorkspace(workspace)
71
+ await this.workspaceService.addWorkspace(clone)
72
+ this.loadWorkspaces()
73
+ }
74
+
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')
81
+ this.loadWorkspaces()
82
+ console.log('[TabbySpaces] loadWorkspaces done, workspaces:', this.workspaces.length)
83
+ }
84
+ }
85
+
86
+ 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 {
93
+ await this.workspaceService.addWorkspace(workspace)
94
+ }
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)
100
+ }
101
+
102
+ closeEditor(): void {
103
+ console.log('[TabbySpaces] closeEditor called, showEditor before:', this.showEditor)
104
+ this.showEditor = false
105
+ this.editingWorkspace = null
106
+ this.cdr.detectChanges()
107
+ console.log('[TabbySpaces] closeEditor done, showEditor after:', this.showEditor)
108
+ }
109
+
110
+ getPaneCount(workspace: Workspace): number {
111
+ return countPanes(workspace.root)
112
+ }
113
+
114
+ getOrientationLabel(workspace: Workspace): string {
115
+ return workspace.root.orientation === 'horizontal' ? 'horizontal' : 'vertical'
116
+ }
117
+
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()
123
+ }
124
+ }