tabby-tabbyspaces 0.0.1 → 0.1.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 (56) hide show
  1. package/.claude/settings.local.json +28 -2
  2. package/CHANGELOG.md +46 -20
  3. package/CLAUDE.md +163 -15
  4. package/README.md +71 -61
  5. package/RELEASE.md +91 -0
  6. package/TEST_MCP.md +176 -0
  7. package/TODO.md +72 -0
  8. package/cdp-click.js +22 -0
  9. package/cdp-test.js +28 -0
  10. package/dist/components/paneEditor.component.d.ts +6 -1
  11. package/dist/components/paneEditor.component.d.ts.map +1 -1
  12. package/dist/components/splitPreview.component.d.ts +22 -7
  13. package/dist/components/splitPreview.component.d.ts.map +1 -1
  14. package/dist/components/workspaceEditor.component.d.ts +30 -4
  15. package/dist/components/workspaceEditor.component.d.ts.map +1 -1
  16. package/dist/components/workspaceList.component.d.ts +21 -9
  17. package/dist/components/workspaceList.component.d.ts.map +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.LICENSE.txt +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/models/workspace.model.d.ts +4 -2
  23. package/dist/models/workspace.model.d.ts.map +1 -1
  24. package/dist/package.json +26 -0
  25. package/dist/providers/settings.provider.d.ts.map +1 -1
  26. package/dist/providers/toolbar.provider.d.ts +4 -1
  27. package/dist/providers/toolbar.provider.d.ts.map +1 -1
  28. package/dist/services/startupCommand.service.d.ts +20 -0
  29. package/dist/services/startupCommand.service.d.ts.map +1 -0
  30. package/dist/services/workspaceEditor.service.d.ts +11 -3
  31. package/dist/services/workspaceEditor.service.d.ts.map +1 -1
  32. package/docs/marketing_status.md +92 -0
  33. package/package.json +2 -7
  34. package/screenshots/editor.png +0 -0
  35. package/screenshots/pane-edit.png +0 -0
  36. package/scripts/build-prod.js +39 -0
  37. package/src/components/paneEditor.component.pug +2 -2
  38. package/src/components/paneEditor.component.ts +19 -1
  39. package/src/components/splitPreview.component.pug +45 -5
  40. package/src/components/splitPreview.component.scss +79 -22
  41. package/src/components/splitPreview.component.ts +91 -16
  42. package/src/components/workspaceEditor.component.pug +130 -70
  43. package/src/components/workspaceEditor.component.scss +205 -120
  44. package/src/components/workspaceEditor.component.ts +193 -6
  45. package/src/components/workspaceList.component.pug +31 -20
  46. package/src/components/workspaceList.component.scss +12 -6
  47. package/src/components/workspaceList.component.ts +116 -34
  48. package/src/index.ts +2 -0
  49. package/src/models/workspace.model.ts +33 -6
  50. package/src/providers/settings.provider.ts +2 -2
  51. package/src/providers/toolbar.provider.ts +41 -10
  52. package/src/services/startupCommand.service.ts +142 -0
  53. package/src/services/workspaceEditor.service.ts +70 -38
  54. package/test_cdp.py +50 -0
  55. package/RELEASE_PLAN.md +0 -161
  56. package/screenshots/workspace-edit.png +0 -0
@@ -1,169 +1,254 @@
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);
1
+ // Workspace editor (inline)
2
+ .workspace-editor-inline {
3
+ background: var(--theme-bg-more);
4
+ border-radius: 8px;
5
+ border: 1px solid var(--theme-border);
6
+ margin-bottom: 16px;
32
7
 
33
- h4 {
34
- margin: 0;
35
- font-size: 1.25rem;
8
+ .editor-body {
9
+ padding: 16px;
36
10
  }
37
11
 
38
- .close-btn {
39
- font-size: 1.25rem;
40
- color: var(--theme-fg-more);
41
- padding: 4px 8px;
12
+ .editor-footer {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ align-items: center;
16
+ padding: 10px 16px;
17
+ border-top: 1px solid var(--theme-border);
18
+ background: var(--theme-bg);
19
+ border-radius: 0 0 8px 8px;
20
+
21
+ .startup-checkbox {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 6px;
25
+ font-size: 0.85rem;
26
+ color: var(--theme-fg-more);
27
+ cursor: pointer;
28
+
29
+ input {
30
+ margin: 0;
31
+ }
32
+ }
42
33
 
43
- &:hover {
44
- color: var(--theme-fg);
34
+ .editor-actions {
35
+ display: flex;
36
+ gap: 8px;
37
+
38
+ .btn-success {
39
+ background: #10b981;
40
+ border-color: #10b981;
41
+ color: white;
42
+
43
+ &:hover {
44
+ background: #059669;
45
+ border-color: #059669;
46
+ }
47
+ }
48
+
49
+ .unsaved-indicator {
50
+ color: #f59e0b;
51
+ font-weight: bold;
52
+ margin-left: 2px;
53
+ }
45
54
  }
46
55
  }
47
56
  }
48
57
 
49
- .modal-body {
50
- padding: 20px;
51
- overflow-y: auto;
52
- flex: 1;
53
- }
54
-
55
- .form-row {
58
+ // Shared editor content
59
+ .editor-top-row {
56
60
  display: flex;
57
- gap: 16px;
61
+ align-items: flex-start;
62
+ gap: 12px;
58
63
  margin-bottom: 16px;
59
-
60
- &:last-child {
61
- margin-bottom: 0;
62
- }
63
64
  }
64
65
 
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
- }
66
+ .color-picker {
67
+ .color-input {
68
+ width: 40px;
69
+ height: 40px;
70
+ padding: 2px;
71
+ border: 1px solid var(--theme-border);
72
+ border-radius: 6px;
73
+ cursor: pointer;
74
+ background: var(--theme-bg);
75
+
76
+ &::-webkit-color-swatch-wrapper {
77
+ padding: 2px;
78
+ }
78
79
 
79
- input[type='checkbox'] {
80
- margin-right: 8px;
80
+ &::-webkit-color-swatch {
81
+ border-radius: 4px;
82
+ border: none;
83
+ }
81
84
  }
82
85
  }
83
86
 
84
- .form-control {
85
- padding: 8px 12px;
87
+ .name-input {
88
+ flex: 1;
89
+ min-width: 150px;
90
+ padding: 10px 12px;
86
91
  border-radius: 6px;
87
92
  border: 1px solid var(--theme-border);
88
- background: var(--theme-bg-more);
93
+ background: var(--theme-bg);
89
94
  color: var(--theme-fg);
90
- font-size: 0.95rem;
95
+ font-size: 1rem;
96
+ font-weight: 500;
91
97
 
92
98
  &:focus {
93
99
  outline: none;
94
100
  border-color: var(--theme-primary);
95
101
  }
96
- }
97
102
 
98
- .color-input {
99
- width: 60px;
100
- height: 36px;
101
- padding: 2px;
102
- cursor: pointer;
103
+ &::placeholder {
104
+ color: var(--theme-fg-more);
105
+ font-weight: 400;
106
+ }
103
107
  }
104
108
 
105
- .icon-selector {
106
- display: flex;
107
- flex-wrap: wrap;
108
- gap: 4px;
109
- }
109
+ .icon-picker {
110
+ position: relative;
110
111
 
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;
112
+ .icon-trigger {
113
+ width: 40px;
114
+ height: 40px;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ border: 1px solid var(--theme-border);
119
+ border-radius: 6px;
120
+ background: var(--theme-bg);
121
+ cursor: pointer;
122
+ font-size: 1.1rem;
123
+ transition: background 0.15s;
123
124
 
124
- &:hover {
125
- background: var(--theme-bg-more-more);
126
- color: var(--theme-fg);
125
+ &:hover {
126
+ background: var(--theme-bg-more);
127
+ }
127
128
  }
128
129
 
129
- &.selected {
130
- border-color: var(--theme-primary);
131
- background: var(--theme-primary);
132
- color: white;
130
+ .icon-dropdown {
131
+ position: absolute;
132
+ top: 100%;
133
+ right: 0;
134
+ margin-top: 4px;
135
+ padding: 8px;
136
+ background: var(--theme-bg-more);
137
+ border: 1px solid var(--theme-border);
138
+ border-radius: 6px;
139
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
140
+ z-index: 100;
141
+ display: grid;
142
+ grid-template-columns: repeat(6, 28px);
143
+ gap: 4px;
133
144
  }
134
- }
135
145
 
136
- .form-section {
137
- margin-top: 20px;
146
+ .icon-option {
147
+ width: 28px;
148
+ height: 28px;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ border: 1px solid transparent;
153
+ border-radius: 4px;
154
+ background: transparent;
155
+ color: var(--theme-fg-more);
156
+ cursor: pointer;
157
+ transition: all 0.15s;
158
+ font-size: 0.85rem;
159
+
160
+ &:hover {
161
+ background: var(--theme-bg-more-more);
162
+ color: var(--theme-fg);
163
+ }
164
+
165
+ &.selected {
166
+ border-color: currentColor;
167
+ background: var(--theme-bg);
168
+ }
169
+ }
170
+ }
138
171
 
139
- .section-header {
172
+ // Layout section
173
+ .layout-section {
174
+ .layout-header {
140
175
  display: flex;
141
176
  justify-content: space-between;
142
177
  align-items: center;
143
- margin-bottom: 12px;
178
+ margin-bottom: 8px;
179
+ }
144
180
 
145
- h5 {
146
- margin: 0;
147
- font-size: 1rem;
148
- }
181
+ .layout-title {
182
+ font-size: 0.8rem;
183
+ font-weight: 500;
184
+ color: var(--theme-fg-more);
185
+ text-transform: uppercase;
186
+ letter-spacing: 0.5px;
149
187
  }
150
188
  }
151
189
 
152
- .orientation-toggle {
190
+ // Layout toolbar
191
+ .layout-toolbar {
153
192
  display: flex;
154
- gap: 8px;
193
+ align-items: center;
194
+ gap: 4px;
195
+ padding: 8px 0;
196
+ margin-bottom: 8px;
155
197
 
156
- .btn {
198
+ .toolbar-group {
157
199
  display: flex;
158
200
  align-items: center;
159
- gap: 6px;
201
+ gap: 2px;
202
+ }
203
+
204
+ .toolbar-label {
205
+ font-size: 0.75rem;
206
+ color: var(--theme-fg-more);
207
+ margin-right: 4px;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.3px;
210
+ }
211
+
212
+ .toolbar-spacer {
213
+ flex: 1;
214
+ }
215
+
216
+ .toolbar-divider {
217
+ width: 1px;
218
+ height: 20px;
219
+ background: var(--theme-border);
220
+ margin: 0 4px;
221
+ }
222
+
223
+ .toolbar-btn {
224
+ padding: 4px 8px;
225
+ background: var(--theme-bg);
226
+ border: 1px solid var(--theme-border);
227
+ border-radius: 4px;
228
+ color: var(--theme-fg);
229
+ cursor: pointer;
230
+ transition: all 0.15s;
231
+ font-size: 0.85rem;
232
+
233
+ &:hover:not(:disabled) {
234
+ background: var(--theme-bg-more-more);
235
+ }
236
+
237
+ &:disabled {
238
+ opacity: 0.4;
239
+ cursor: not-allowed;
240
+ }
241
+
242
+ &.danger {
243
+ color: var(--theme-danger, #ef4444);
244
+ }
160
245
  }
161
246
  }
162
247
 
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);
248
+ .layout-preview-container {
249
+ background: var(--theme-bg);
250
+ border-radius: 6px;
251
+ padding: 8px;
252
+ min-height: 120px;
253
+ max-height: 180px;
169
254
  }
@@ -1,4 +1,4 @@
1
- import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'
1
+ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, SimpleChanges, HostListener, ElementRef, ViewChild } from '@angular/core'
2
2
  import {
3
3
  Workspace,
4
4
  WorkspacePane,
@@ -15,12 +15,17 @@ import { WorkspaceEditorService } from '../services/workspaceEditor.service'
15
15
  template: require('./workspaceEditor.component.pug'),
16
16
  styles: [require('./workspaceEditor.component.scss')],
17
17
  })
18
- export class WorkspaceEditorComponent implements OnInit {
18
+ export class WorkspaceEditorComponent implements OnInit, OnChanges, AfterViewInit {
19
19
  @Input() workspace!: Workspace
20
+ @Input() autoFocus = false
21
+ @Input() hasUnsavedChanges = false
20
22
  @Output() save = new EventEmitter<Workspace>()
21
23
  @Output() cancel = new EventEmitter<void>()
22
24
 
23
- selectedPane: WorkspacePane | null = null
25
+ @ViewChild('nameInput') nameInput!: ElementRef<HTMLInputElement>
26
+
27
+ selectedPaneId: string | null = null
28
+ editingPane: WorkspacePane | null = null
24
29
  showPaneEditor = false
25
30
  profiles: TabbyProfile[] = []
26
31
  availableIcons = [
@@ -28,11 +33,69 @@ export class WorkspaceEditorComponent implements OnInit {
28
33
  'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
29
34
  'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
30
35
  ]
36
+ iconDropdownOpen = false
37
+
38
+ constructor(
39
+ private workspaceService: WorkspaceEditorService,
40
+ private elementRef: ElementRef
41
+ ) {}
42
+
43
+ @HostListener('document:click', ['$event'])
44
+ onDocumentClick(event: MouseEvent): void {
45
+ const iconPicker = this.elementRef.nativeElement.querySelector('.icon-picker')
46
+ if (iconPicker && !iconPicker.contains(event.target as Node)) {
47
+ this.iconDropdownOpen = false
48
+ }
49
+ }
31
50
 
32
- constructor(private workspaceService: WorkspaceEditorService) {}
51
+ toggleIconDropdown(): void {
52
+ this.iconDropdownOpen = !this.iconDropdownOpen
53
+ }
54
+
55
+ selectIcon(icon: string): void {
56
+ this.workspace.icon = icon
57
+ this.iconDropdownOpen = false
58
+ }
33
59
 
34
60
  async ngOnInit(): Promise<void> {
35
61
  this.profiles = await this.workspaceService.getAvailableProfiles()
62
+ this.initializeWorkspace()
63
+ }
64
+
65
+ ngAfterViewInit(): void {
66
+ if (this.autoFocus) {
67
+ this.focusNameInput()
68
+ }
69
+ }
70
+
71
+ private focusNameInput(): void {
72
+ requestAnimationFrame(() => {
73
+ setTimeout(() => {
74
+ if (this.nameInput?.nativeElement) {
75
+ this.nameInput.nativeElement.focus()
76
+ this.nameInput.nativeElement.select()
77
+ }
78
+ }, 0)
79
+ })
80
+ }
81
+
82
+ ngOnChanges(changes: SimpleChanges): void {
83
+ if (changes['workspace'] && !changes['workspace'].firstChange) {
84
+ // Reset component state when workspace input changes
85
+ this.selectedPaneId = null
86
+ this.editingPane = null
87
+ this.showPaneEditor = false
88
+ this.iconDropdownOpen = false
89
+ this.initializeWorkspace()
90
+ }
91
+
92
+ // Handle autoFocus change
93
+ if (changes['autoFocus']?.currentValue) {
94
+ this.focusNameInput()
95
+ }
96
+ }
97
+
98
+ private initializeWorkspace(): void {
36
99
  if (!this.workspace.root) {
37
100
  this.workspace.root = {
38
101
  orientation: 'horizontal',
@@ -54,13 +117,31 @@ export class WorkspaceEditorComponent implements OnInit {
54
117
  }
55
118
 
56
119
  selectPane(pane: WorkspacePane): void {
57
- this.selectedPane = pane
120
+ this.selectedPaneId = pane.id
121
+ }
122
+
123
+ deselectPane(): void {
124
+ this.selectedPaneId = null
125
+ }
126
+
127
+ onPreviewBackgroundClick(): void {
128
+ this.deselectPane()
129
+ }
130
+
131
+ editPane(pane: WorkspacePane): void {
132
+ this.editingPane = pane
58
133
  this.showPaneEditor = true
59
134
  }
60
135
 
136
+ editSelectedPane(): void {
137
+ if (!this.selectedPaneId) return
138
+ const pane = this.findPaneById(this.selectedPaneId)
139
+ if (pane) this.editPane(pane)
140
+ }
141
+
61
142
  closePaneEditor(): void {
62
143
  this.showPaneEditor = false
63
- this.selectedPane = null
144
+ this.editingPane = null
64
145
  }
65
146
 
66
147
  onPaneSave(pane: WorkspacePane): void {
@@ -68,6 +149,33 @@ export class WorkspaceEditorComponent implements OnInit {
68
149
  this.closePaneEditor()
69
150
  }
70
151
 
152
+ // Helper functions
153
+ private findPaneById(id: string): WorkspacePane | null {
154
+ return this.findPaneInNode(this.workspace.root, id)
155
+ }
156
+
157
+ private findPaneInNode(node: WorkspaceSplit, id: string): WorkspacePane | null {
158
+ for (const child of node.children) {
159
+ if (isWorkspaceSplit(child)) {
160
+ const found = this.findPaneInNode(child, id)
161
+ if (found) return found
162
+ } else if (child.id === id) {
163
+ return child
164
+ }
165
+ }
166
+ return null
167
+ }
168
+
169
+ canRemovePane(): boolean {
170
+ return this.countPanes(this.workspace.root) > 1
171
+ }
172
+
173
+ private countPanes(node: WorkspaceSplit): number {
174
+ return node.children.reduce((count, child) => {
175
+ return count + (isWorkspaceSplit(child) ? this.countPanes(child) : 1)
176
+ }, 0)
177
+ }
178
+
71
179
  private updatePaneInTree(node: WorkspaceSplit, updatedPane: WorkspacePane): boolean {
72
180
  for (let i = 0; i < node.children.length; i++) {
73
181
  const child = node.children[i]
@@ -87,6 +195,12 @@ export class WorkspaceEditorComponent implements OnInit {
87
195
  this.splitPaneInTree(this.workspace.root, pane, orientation)
88
196
  }
89
197
 
198
+ splitSelectedPane(orientation: 'horizontal' | 'vertical'): void {
199
+ if (!this.selectedPaneId) return
200
+ const pane = this.findPaneById(this.selectedPaneId)
201
+ if (pane) this.splitPane(pane, orientation)
202
+ }
203
+
90
204
  private splitPaneInTree(
91
205
  node: WorkspaceSplit,
92
206
  targetPane: WorkspacePane,
@@ -115,9 +229,18 @@ export class WorkspaceEditorComponent implements OnInit {
115
229
  }
116
230
 
117
231
  removePane(pane: WorkspacePane): void {
232
+ if (this.selectedPaneId === pane.id) {
233
+ this.selectedPaneId = null
234
+ }
118
235
  this.removePaneFromTree(this.workspace.root, pane)
119
236
  }
120
237
 
238
+ removeSelectedPane(): void {
239
+ if (!this.selectedPaneId || !this.canRemovePane()) return
240
+ const pane = this.findPaneById(this.selectedPaneId)
241
+ if (pane) this.removePane(pane)
242
+ }
243
+
121
244
  private removePaneFromTree(node: WorkspaceSplit, targetPane: WorkspacePane): boolean {
122
245
  for (let i = 0; i < node.children.length; i++) {
123
246
  const child = node.children[i]
@@ -178,4 +301,68 @@ export class WorkspaceEditorComponent implements OnInit {
178
301
  this.workspace.root.ratios = ratios
179
302
  }
180
303
 
304
+ // Add pane operations
305
+ addPane(direction: 'left' | 'right' | 'top' | 'bottom'): void {
306
+ if (!this.selectedPaneId) return
307
+ const pane = this.findPaneById(this.selectedPaneId)
308
+ if (!pane) return
309
+ this.addPaneInTree(this.workspace.root, pane, direction, null)
310
+ }
311
+
312
+ addPaneFromEvent(pane: WorkspacePane, direction: 'left' | 'right' | 'top' | 'bottom'): void {
313
+ this.addPaneInTree(this.workspace.root, pane, direction, null)
314
+ }
315
+
316
+ private addPaneInTree(
317
+ node: WorkspaceSplit,
318
+ targetPane: WorkspacePane,
319
+ direction: 'left' | 'right' | 'top' | 'bottom',
320
+ parentNode: WorkspaceSplit | null
321
+ ): boolean {
322
+ const isHorizontalAdd = direction === 'left' || direction === 'right'
323
+ const isBefore = direction === 'left' || direction === 'top'
324
+ const targetOrientation = isHorizontalAdd ? 'horizontal' : 'vertical'
325
+
326
+ for (let i = 0; i < node.children.length; i++) {
327
+ const child = node.children[i]
328
+
329
+ if (isWorkspaceSplit(child)) {
330
+ if (this.addPaneInTree(child, targetPane, direction, node)) return true
331
+ } else if (child.id === targetPane.id) {
332
+ const newPane = createDefaultPane()
333
+ newPane.profileId = child.profileId
334
+
335
+ if (node.orientation === targetOrientation) {
336
+ // Same orientation: add as sibling
337
+ const insertIndex = isBefore ? i : i + 1
338
+ node.children.splice(insertIndex, 0, newPane)
339
+ this.recalculateRatios(node)
340
+ } else {
341
+ // Different orientation: wrap entire node in new split
342
+ const nodeCopy: WorkspaceSplit = {
343
+ orientation: node.orientation,
344
+ ratios: [...node.ratios],
345
+ children: [...node.children]
346
+ }
347
+ const wrapper: WorkspaceSplit = {
348
+ orientation: targetOrientation,
349
+ ratios: [0.5, 0.5],
350
+ children: isBefore ? [newPane, nodeCopy] : [nodeCopy, newPane]
351
+ }
352
+
353
+ if (node === this.workspace.root) {
354
+ this.workspace.root = wrapper
355
+ } else if (parentNode) {
356
+ const nodeIndex = parentNode.children.indexOf(node)
357
+ if (nodeIndex !== -1) {
358
+ parentNode.children[nodeIndex] = wrapper
359
+ }
360
+ }
361
+ }
362
+ return true
363
+ }
364
+ }
365
+ return false
366
+ }
367
+
181
368
  }