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.
- package/.claude/settings.local.json +28 -2
- package/CHANGELOG.md +46 -20
- package/CLAUDE.md +163 -15
- package/README.md +71 -61
- package/RELEASE.md +91 -0
- package/TEST_MCP.md +176 -0
- package/TODO.md +72 -0
- package/cdp-click.js +22 -0
- package/cdp-test.js +28 -0
- package/dist/components/paneEditor.component.d.ts +6 -1
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +22 -7
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +30 -4
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +21 -9
- package/dist/components/workspaceList.component.d.ts.map +1 -1
- 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 +4 -2
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +26 -0
- package/dist/providers/settings.provider.d.ts.map +1 -1
- package/dist/providers/toolbar.provider.d.ts +4 -1
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +20 -0
- package/dist/services/startupCommand.service.d.ts.map +1 -0
- package/dist/services/workspaceEditor.service.d.ts +11 -3
- package/dist/services/workspaceEditor.service.d.ts.map +1 -1
- package/docs/marketing_status.md +92 -0
- package/package.json +2 -7
- package/screenshots/editor.png +0 -0
- package/screenshots/pane-edit.png +0 -0
- package/scripts/build-prod.js +39 -0
- package/src/components/paneEditor.component.pug +2 -2
- package/src/components/paneEditor.component.ts +19 -1
- package/src/components/splitPreview.component.pug +45 -5
- package/src/components/splitPreview.component.scss +79 -22
- package/src/components/splitPreview.component.ts +91 -16
- package/src/components/workspaceEditor.component.pug +130 -70
- package/src/components/workspaceEditor.component.scss +205 -120
- package/src/components/workspaceEditor.component.ts +193 -6
- package/src/components/workspaceList.component.pug +31 -20
- package/src/components/workspaceList.component.scss +12 -6
- package/src/components/workspaceList.component.ts +116 -34
- package/src/index.ts +2 -0
- package/src/models/workspace.model.ts +33 -6
- package/src/providers/settings.provider.ts +2 -2
- package/src/providers/toolbar.provider.ts +41 -10
- package/src/services/startupCommand.service.ts +142 -0
- package/src/services/workspaceEditor.service.ts +70 -38
- package/test_cdp.py +50 -0
- package/RELEASE_PLAN.md +0 -161
- package/screenshots/workspace-edit.png +0 -0
|
@@ -5,8 +5,23 @@
|
|
|
5
5
|
i.fas.fa-plus
|
|
6
6
|
| New Workspace
|
|
7
7
|
|
|
8
|
+
//- Editor (above list)
|
|
9
|
+
workspace-editor(
|
|
10
|
+
*ngIf='editingWorkspace',
|
|
11
|
+
[workspace]='editingWorkspace',
|
|
12
|
+
[autoFocus]='isCreatingNew',
|
|
13
|
+
[hasUnsavedChanges]='hasUnsavedChanges',
|
|
14
|
+
(save)='onEditorSave($event)',
|
|
15
|
+
(cancel)='onEditorCancel()'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
//- Workspace list
|
|
8
19
|
.workspace-list(*ngIf='workspaces.length > 0')
|
|
9
|
-
.workspace-item(
|
|
20
|
+
.workspace-item(
|
|
21
|
+
*ngFor='let workspace of workspaces',
|
|
22
|
+
[class.selected]='isSelected(workspace)',
|
|
23
|
+
(click)='selectWorkspace(workspace)'
|
|
24
|
+
)
|
|
10
25
|
.workspace-info
|
|
11
26
|
.workspace-icon([style.color]='workspace.color')
|
|
12
27
|
i.fas([class]='"fa-" + (workspace.icon || "columns")')
|
|
@@ -16,31 +31,27 @@
|
|
|
16
31
|
span {{ getPaneCount(workspace) }} panes
|
|
17
32
|
span.separator ·
|
|
18
33
|
span {{ getOrientationLabel(workspace) }}
|
|
19
|
-
span.separator(*ngIf='workspace.
|
|
20
|
-
span.badge.badge-primary(*ngIf='workspace.
|
|
34
|
+
span.separator(*ngIf='workspace.launchOnStartup') ·
|
|
35
|
+
span.badge.badge-primary(*ngIf='workspace.launchOnStartup') startup
|
|
21
36
|
|
|
22
37
|
.workspace-actions
|
|
23
|
-
button.btn.btn-link(
|
|
38
|
+
button.btn.btn-link.open-btn(
|
|
24
39
|
type='button',
|
|
25
|
-
title='
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
title='Open',
|
|
41
|
+
[disabled]='openingWorkspaceId === workspace.id',
|
|
42
|
+
(click)='openWorkspace($event, workspace)'
|
|
28
43
|
)
|
|
29
|
-
i.fas
|
|
30
|
-
|
|
44
|
+
i.fas(
|
|
45
|
+
[class.fa-external-link-alt]='openingWorkspaceId !== workspace.id',
|
|
46
|
+
[class.fa-spinner]='openingWorkspaceId === workspace.id',
|
|
47
|
+
[class.fa-spin]='openingWorkspaceId === workspace.id'
|
|
48
|
+
)
|
|
49
|
+
button.btn.btn-link(type='button', title='Duplicate', (click)='duplicateWorkspace($event, workspace)')
|
|
31
50
|
i.fas.fa-copy
|
|
32
|
-
button.btn.btn-link(type='button', title='
|
|
33
|
-
i.fas.fa-edit
|
|
34
|
-
button.btn.btn-link.text-danger(type='button', title='Delete', (click)='deleteWorkspace(workspace)')
|
|
51
|
+
button.btn.btn-link.text-danger(type='button', title='Delete', (click)='deleteWorkspace($event, workspace)')
|
|
35
52
|
i.fas.fa-trash
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
//- Empty state
|
|
55
|
+
.workspace-empty(*ngIf='workspaces.length === 0 && !isCreatingNew')
|
|
38
56
|
p No workspaces configured yet.
|
|
39
57
|
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
|
-
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
.workspace-list-container {
|
|
2
2
|
padding: 20px;
|
|
3
|
-
max-width: 800px;
|
|
4
3
|
}
|
|
5
4
|
|
|
6
5
|
.workspace-list-header {
|
|
@@ -29,11 +28,19 @@
|
|
|
29
28
|
background: var(--theme-bg-more);
|
|
30
29
|
border-radius: 8px;
|
|
31
30
|
border: 1px solid var(--theme-border);
|
|
32
|
-
transition: background 0.2s;
|
|
31
|
+
transition: background 0.2s, border-color 0.2s;
|
|
32
|
+
cursor: pointer;
|
|
33
33
|
|
|
34
34
|
&:hover {
|
|
35
35
|
background: var(--theme-bg-more-more);
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
&.selected {
|
|
39
|
+
border-color: var(--theme-primary);
|
|
40
|
+
border-left-width: 3px;
|
|
41
|
+
background: var(--theme-bg-more-more);
|
|
42
|
+
box-shadow: 0 0 0 1px var(--theme-primary) inset;
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
.workspace-info {
|
|
@@ -84,15 +91,14 @@
|
|
|
84
91
|
padding: 8px;
|
|
85
92
|
color: var(--theme-fg-more);
|
|
86
93
|
opacity: 0.7;
|
|
87
|
-
transition: opacity 0.2s;
|
|
94
|
+
transition: opacity 0.2s, color 0.2s;
|
|
88
95
|
|
|
89
96
|
&:hover {
|
|
90
97
|
opacity: 1;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
&.
|
|
94
|
-
color:
|
|
95
|
-
opacity: 1;
|
|
100
|
+
&.open-btn:hover {
|
|
101
|
+
color: var(--theme-success, #10b981);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
&.text-danger:hover {
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { ConfigService, ProfilesService } from 'tabby-core'
|
|
3
3
|
import { Subscription } from 'rxjs'
|
|
4
|
+
import { StartupCommandService } from '../services/startupCommand.service'
|
|
4
5
|
import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
5
6
|
import {
|
|
6
7
|
Workspace,
|
|
@@ -16,25 +17,58 @@ import {
|
|
|
16
17
|
template: require('./workspaceList.component.pug'),
|
|
17
18
|
styles: [require('./workspaceList.component.scss')],
|
|
18
19
|
})
|
|
19
|
-
export class WorkspaceListComponent implements OnInit, OnDestroy {
|
|
20
|
+
export class WorkspaceListComponent implements OnInit, OnDestroy, AfterViewInit {
|
|
20
21
|
workspaces: Workspace[] = []
|
|
22
|
+
selectedWorkspace: Workspace | null = null
|
|
21
23
|
editingWorkspace: Workspace | null = null
|
|
22
|
-
|
|
24
|
+
isCreatingNew = false
|
|
25
|
+
openingWorkspaceId: string | null = null
|
|
23
26
|
private configSubscription: Subscription | null = null
|
|
24
27
|
|
|
25
28
|
constructor(
|
|
26
29
|
public config: ConfigService,
|
|
27
30
|
private workspaceService: WorkspaceEditorService,
|
|
28
|
-
private
|
|
31
|
+
private profilesService: ProfilesService,
|
|
32
|
+
private startupService: StartupCommandService,
|
|
33
|
+
private cdr: ChangeDetectorRef,
|
|
34
|
+
private elementRef: ElementRef,
|
|
35
|
+
private zone: NgZone
|
|
29
36
|
) {}
|
|
30
37
|
|
|
31
38
|
ngOnInit(): void {
|
|
32
39
|
this.loadWorkspaces()
|
|
40
|
+
this.autoSelectFirst()
|
|
33
41
|
this.configSubscription = this.config.changed$.subscribe(() => {
|
|
34
42
|
this.loadWorkspaces()
|
|
35
43
|
})
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
ngAfterViewInit(): void {
|
|
47
|
+
// Hack: Override Tabby's settings-tab-body max-width restriction
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
const parent = this.elementRef.nativeElement.closest('settings-tab-body') as HTMLElement
|
|
50
|
+
if (parent) {
|
|
51
|
+
parent.style.maxWidth = '876px'
|
|
52
|
+
}
|
|
53
|
+
}, 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private autoSelectFirst(): void {
|
|
57
|
+
if (this.workspaces.length > 0 && !this.selectedWorkspace) {
|
|
58
|
+
this.selectWorkspace(this.workspaces[0])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
selectWorkspace(workspace: Workspace): void {
|
|
63
|
+
this.isCreatingNew = false
|
|
64
|
+
this.selectedWorkspace = workspace
|
|
65
|
+
this.editingWorkspace = JSON.parse(JSON.stringify(workspace))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isSelected(workspace: Workspace): boolean {
|
|
69
|
+
return this.selectedWorkspace?.id === workspace.id
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
ngOnDestroy(): void {
|
|
39
73
|
this.configSubscription?.unsubscribe()
|
|
40
74
|
}
|
|
@@ -49,8 +83,10 @@ export class WorkspaceListComponent implements OnInit, OnDestroy {
|
|
|
49
83
|
const defaultProfileId = profiles[0]?.id || ''
|
|
50
84
|
const workspace = createDefaultWorkspace()
|
|
51
85
|
this.setProfileForAllPanes(workspace.root, defaultProfileId)
|
|
86
|
+
this.selectedWorkspace = null
|
|
52
87
|
this.editingWorkspace = workspace
|
|
53
|
-
this.
|
|
88
|
+
this.isCreatingNew = true
|
|
89
|
+
this.cdr.detectChanges()
|
|
54
90
|
}
|
|
55
91
|
|
|
56
92
|
private setProfileForAllPanes(node: WorkspacePane | WorkspaceSplit, profileId: string): void {
|
|
@@ -62,49 +98,76 @@ export class WorkspaceListComponent implements OnInit, OnDestroy {
|
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
editWorkspace(workspace: Workspace): void {
|
|
65
|
-
this.
|
|
66
|
-
this.showEditor = true
|
|
101
|
+
this.selectWorkspace(workspace)
|
|
67
102
|
}
|
|
68
103
|
|
|
69
|
-
async duplicateWorkspace(workspace: Workspace): Promise<void> {
|
|
104
|
+
async duplicateWorkspace(event: MouseEvent, workspace: Workspace): Promise<void> {
|
|
105
|
+
event.stopPropagation()
|
|
70
106
|
const clone = this.workspaceService.duplicateWorkspace(workspace)
|
|
71
107
|
await this.workspaceService.addWorkspace(clone)
|
|
72
108
|
this.loadWorkspaces()
|
|
109
|
+
|
|
110
|
+
// Select the duplicated workspace
|
|
111
|
+
const duplicated = this.workspaces.find((w) => w.id === clone.id)
|
|
112
|
+
if (duplicated) {
|
|
113
|
+
this.selectWorkspace(duplicated)
|
|
114
|
+
}
|
|
115
|
+
this.cdr.detectChanges()
|
|
73
116
|
}
|
|
74
117
|
|
|
75
|
-
async deleteWorkspace(workspace: Workspace): Promise<void> {
|
|
76
|
-
|
|
118
|
+
async deleteWorkspace(event: MouseEvent, workspace: Workspace): Promise<void> {
|
|
119
|
+
event.stopPropagation()
|
|
77
120
|
if (confirm(`Delete workspace "${workspace.name}"?`)) {
|
|
78
|
-
|
|
121
|
+
const currentIndex = this.workspaces.findIndex((w) => w.id === workspace.id)
|
|
79
122
|
await this.workspaceService.deleteWorkspace(workspace.id)
|
|
80
|
-
console.log('[TabbySpaces] service.deleteWorkspace done, calling loadWorkspaces')
|
|
81
123
|
this.loadWorkspaces()
|
|
82
|
-
|
|
124
|
+
|
|
125
|
+
// Select next workspace after deletion
|
|
126
|
+
if (this.workspaces.length > 0) {
|
|
127
|
+
const nextIndex = Math.min(currentIndex, this.workspaces.length - 1)
|
|
128
|
+
this.selectWorkspace(this.workspaces[nextIndex])
|
|
129
|
+
} else {
|
|
130
|
+
this.selectedWorkspace = null
|
|
131
|
+
this.editingWorkspace = null
|
|
132
|
+
this.isCreatingNew = false
|
|
133
|
+
}
|
|
134
|
+
this.cdr.detectChanges()
|
|
83
135
|
}
|
|
84
136
|
}
|
|
85
137
|
|
|
86
138
|
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 {
|
|
139
|
+
const isNew = !this.workspaces.find((w) => w.id === workspace.id)
|
|
140
|
+
if (isNew) {
|
|
93
141
|
await this.workspaceService.addWorkspace(workspace)
|
|
142
|
+
} else {
|
|
143
|
+
await this.workspaceService.updateWorkspace(workspace)
|
|
94
144
|
}
|
|
95
|
-
console.log('[TabbySpaces] save done, calling loadWorkspaces')
|
|
96
145
|
this.loadWorkspaces()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
146
|
+
this.isCreatingNew = false
|
|
147
|
+
|
|
148
|
+
// Select the saved workspace
|
|
149
|
+
const saved = this.workspaces.find((w) => w.id === workspace.id)
|
|
150
|
+
if (saved) {
|
|
151
|
+
this.selectWorkspace(saved)
|
|
152
|
+
}
|
|
153
|
+
this.cdr.detectChanges()
|
|
100
154
|
}
|
|
101
155
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
156
|
+
onEditorCancel(): void {
|
|
157
|
+
if (this.isCreatingNew) {
|
|
158
|
+
// Cancel new workspace creation - go back to first workspace or empty
|
|
159
|
+
this.isCreatingNew = false
|
|
160
|
+
if (this.workspaces.length > 0) {
|
|
161
|
+
this.selectWorkspace(this.workspaces[0])
|
|
162
|
+
} else {
|
|
163
|
+
this.selectedWorkspace = null
|
|
164
|
+
this.editingWorkspace = null
|
|
165
|
+
}
|
|
166
|
+
} else if (this.selectedWorkspace) {
|
|
167
|
+
// Reset to original workspace data
|
|
168
|
+
this.editingWorkspace = JSON.parse(JSON.stringify(this.selectedWorkspace))
|
|
169
|
+
}
|
|
106
170
|
this.cdr.detectChanges()
|
|
107
|
-
console.log('[TabbySpaces] closeEditor done, showEditor after:', this.showEditor)
|
|
108
171
|
}
|
|
109
172
|
|
|
110
173
|
getPaneCount(workspace: Workspace): number {
|
|
@@ -115,10 +178,29 @@ export class WorkspaceListComponent implements OnInit, OnDestroy {
|
|
|
115
178
|
return workspace.root.orientation === 'horizontal' ? 'horizontal' : 'vertical'
|
|
116
179
|
}
|
|
117
180
|
|
|
118
|
-
|
|
119
|
-
this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
181
|
+
get hasUnsavedChanges(): boolean {
|
|
182
|
+
if (!this.editingWorkspace || !this.selectedWorkspace) return this.isCreatingNew
|
|
183
|
+
return JSON.stringify(this.editingWorkspace) !== JSON.stringify(this.selectedWorkspace)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async openWorkspace(event: MouseEvent, workspace: Workspace): Promise<void> {
|
|
187
|
+
event.stopPropagation()
|
|
188
|
+
if (this.openingWorkspaceId) return
|
|
189
|
+
this.openingWorkspaceId = workspace.id
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const commands = this.workspaceService.collectStartupCommands(workspace)
|
|
193
|
+
if (commands.length > 0) {
|
|
194
|
+
this.startupService.registerCommands(commands)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const profile = await this.workspaceService.generateTabbyProfile(workspace)
|
|
198
|
+
this.zone.run(() => {
|
|
199
|
+
this.profilesService.openNewTabForProfile(profile)
|
|
200
|
+
})
|
|
201
|
+
} finally {
|
|
202
|
+
this.openingWorkspaceId = null
|
|
203
|
+
this.cdr.detectChanges()
|
|
204
|
+
}
|
|
123
205
|
}
|
|
124
206
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ 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'
|
|
11
12
|
|
|
12
13
|
import { WorkspaceListComponent } from './components/workspaceList.component'
|
|
13
14
|
import { WorkspaceEditorComponent } from './components/workspaceEditor.component'
|
|
@@ -21,6 +22,7 @@ import { SplitPreviewComponent } from './components/splitPreview.component'
|
|
|
21
22
|
{ provide: SettingsTabProvider, useClass: WorkspaceEditorSettingsProvider, multi: true },
|
|
22
23
|
{ provide: ToolbarButtonProvider, useClass: WorkspaceToolbarProvider, multi: true },
|
|
23
24
|
WorkspaceEditorService,
|
|
25
|
+
StartupCommandService,
|
|
24
26
|
],
|
|
25
27
|
declarations: [
|
|
26
28
|
WorkspaceListComponent,
|
|
@@ -37,6 +37,7 @@ export interface TabbyRecoveryToken {
|
|
|
37
37
|
tabTitle?: string
|
|
38
38
|
tabCustomTitle?: string
|
|
39
39
|
disableDynamicTitle?: boolean
|
|
40
|
+
cwd?: string
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export interface TabbySplitLayoutProfile {
|
|
@@ -73,8 +74,7 @@ export interface Workspace {
|
|
|
73
74
|
icon?: string
|
|
74
75
|
color?: string
|
|
75
76
|
root: WorkspaceSplit
|
|
76
|
-
|
|
77
|
-
hotkey?: string
|
|
77
|
+
launchOnStartup?: boolean
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function isWorkspaceSplit(node: WorkspacePane | WorkspaceSplit): node is WorkspaceSplit {
|
|
@@ -99,14 +99,41 @@ export function createDefaultSplit(orientation: 'horizontal' | 'vertical' = 'hor
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// Color palette for workspaces
|
|
103
|
+
const WORKSPACE_COLORS = [
|
|
104
|
+
'#3b82f6', // blue
|
|
105
|
+
'#10b981', // emerald
|
|
106
|
+
'#f59e0b', // amber
|
|
107
|
+
'#ef4444', // red
|
|
108
|
+
'#8b5cf6', // violet
|
|
109
|
+
'#ec4899', // pink
|
|
110
|
+
'#06b6d4', // cyan
|
|
111
|
+
'#f97316', // orange
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
// Icon list for workspaces
|
|
115
|
+
const WORKSPACE_ICONS = [
|
|
116
|
+
'columns', 'terminal', 'code', 'folder', 'home', 'briefcase',
|
|
117
|
+
'cog', 'database', 'server', 'cloud', 'rocket', 'flask',
|
|
118
|
+
'bug', 'wrench', 'cube', 'layer-group', 'sitemap', 'project-diagram'
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
export function getRandomColor(): string {
|
|
122
|
+
return WORKSPACE_COLORS[Math.floor(Math.random() * WORKSPACE_COLORS.length)]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getRandomIcon(): string {
|
|
126
|
+
return WORKSPACE_ICONS[Math.floor(Math.random() * WORKSPACE_ICONS.length)]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createDefaultWorkspace(name: string = ''): Workspace {
|
|
103
130
|
return {
|
|
104
131
|
id: generateUUID(),
|
|
105
132
|
name,
|
|
106
|
-
icon:
|
|
107
|
-
color:
|
|
133
|
+
icon: getRandomIcon(),
|
|
134
|
+
color: getRandomColor(),
|
|
108
135
|
root: createDefaultSplit(),
|
|
109
|
-
|
|
136
|
+
launchOnStartup: false,
|
|
110
137
|
}
|
|
111
138
|
}
|
|
112
139
|
|
|
@@ -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 {
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core'
|
|
2
2
|
import { ToolbarButtonProvider, ToolbarButton, ProfilesService, AppService } from 'tabby-core'
|
|
3
3
|
import { WorkspaceEditorService } from '../services/workspaceEditor.service'
|
|
4
|
+
import { StartupCommandService } from '../services/startupCommand.service'
|
|
4
5
|
import { SettingsTabComponent } from 'tabby-settings'
|
|
5
|
-
import { CONFIG_KEY, DISPLAY_NAME } from '../build-config'
|
|
6
|
+
import { CONFIG_KEY, DISPLAY_NAME, IS_DEV } from '../build-config'
|
|
7
|
+
|
|
8
|
+
const ICON_GRID = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9
|
+
<rect x="3" y="3" width="7" height="7"/>
|
|
10
|
+
<rect x="14" y="3" width="7" height="7"/>
|
|
11
|
+
<rect x="14" y="14" width="7" height="7"/>
|
|
12
|
+
<rect x="3" y="14" width="7" height="7"/>
|
|
13
|
+
</svg>`
|
|
14
|
+
|
|
15
|
+
const ICON_BOLT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
16
|
+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
17
|
+
</svg>`
|
|
6
18
|
import { countPanes } from '../models/workspace.model'
|
|
7
19
|
|
|
8
20
|
@Injectable()
|
|
@@ -10,20 +22,32 @@ export class WorkspaceToolbarProvider extends ToolbarButtonProvider {
|
|
|
10
22
|
constructor(
|
|
11
23
|
private workspaceService: WorkspaceEditorService,
|
|
12
24
|
private profilesService: ProfilesService,
|
|
13
|
-
private app: AppService
|
|
25
|
+
private app: AppService,
|
|
26
|
+
private startupService: StartupCommandService
|
|
14
27
|
) {
|
|
15
28
|
super()
|
|
29
|
+
// Delay startup tasks to ensure Tabby config is loaded
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
// Cleanup orphaned profiles from previous plugin versions (one-time migration)
|
|
32
|
+
this.workspaceService.cleanupOrphanedProfiles()
|
|
33
|
+
// Launch workspaces marked for startup
|
|
34
|
+
this.launchStartupWorkspaces()
|
|
35
|
+
}, 500)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async launchStartupWorkspaces(): Promise<void> {
|
|
39
|
+
const workspaces = this.workspaceService.getWorkspaces()
|
|
40
|
+
const startupWorkspaces = workspaces.filter(w => w.launchOnStartup)
|
|
41
|
+
|
|
42
|
+
for (const workspace of startupWorkspaces) {
|
|
43
|
+
await this.openWorkspace(workspace.id)
|
|
44
|
+
}
|
|
16
45
|
}
|
|
17
46
|
|
|
18
47
|
provide(): ToolbarButton[] {
|
|
19
48
|
return [
|
|
20
49
|
{
|
|
21
|
-
icon:
|
|
22
|
-
<rect x="3" y="3" width="7" height="7"/>
|
|
23
|
-
<rect x="14" y="3" width="7" height="7"/>
|
|
24
|
-
<rect x="14" y="14" width="7" height="7"/>
|
|
25
|
-
<rect x="3" y="14" width="7" height="7"/>
|
|
26
|
-
</svg>`,
|
|
50
|
+
icon: IS_DEV ? ICON_BOLT : ICON_GRID,
|
|
27
51
|
title: DISPLAY_NAME,
|
|
28
52
|
weight: 5,
|
|
29
53
|
click: () => this.showWorkspaceSelector()
|
|
@@ -69,13 +93,20 @@ export class WorkspaceToolbarProvider extends ToolbarButtonProvider {
|
|
|
69
93
|
this.app.openNewTabRaw({ type: SettingsTabComponent, inputs: { activeTab: CONFIG_KEY } })
|
|
70
94
|
}
|
|
71
95
|
|
|
72
|
-
private openWorkspace(workspaceId: string): void {
|
|
96
|
+
private async openWorkspace(workspaceId: string): Promise<void> {
|
|
73
97
|
const workspaces = this.workspaceService.getWorkspaces()
|
|
74
98
|
const workspace = workspaces.find((w) => w.id === workspaceId)
|
|
75
99
|
|
|
76
100
|
if (!workspace) return
|
|
77
101
|
|
|
78
|
-
|
|
102
|
+
// Register startup commands BEFORE opening the workspace
|
|
103
|
+
// Commands will be sent via sendInput() when terminals open
|
|
104
|
+
const commands = this.workspaceService.collectStartupCommands(workspace)
|
|
105
|
+
if (commands.length > 0) {
|
|
106
|
+
this.startupService.registerCommands(commands)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const profile = await this.workspaceService.generateTabbyProfile(workspace)
|
|
79
110
|
this.profilesService.openNewTabForProfile(profile)
|
|
80
111
|
}
|
|
81
112
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core'
|
|
2
|
+
import { AppService, BaseTabComponent, SplitTabComponent } from 'tabby-core'
|
|
3
|
+
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
|
4
|
+
import { Subscription, first, timer, switchMap } from 'rxjs'
|
|
5
|
+
|
|
6
|
+
export interface PendingCommand {
|
|
7
|
+
paneId: string
|
|
8
|
+
command?: string
|
|
9
|
+
originalTitle: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class StartupCommandService {
|
|
14
|
+
private pendingCommands: Map<string, PendingCommand> = new Map()
|
|
15
|
+
private subscription: Subscription
|
|
16
|
+
|
|
17
|
+
constructor(private app: AppService) {
|
|
18
|
+
this.subscription = this.app.tabOpened$.subscribe((tab) => this.onTabOpened(tab))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
registerCommands(commands: PendingCommand[]): void {
|
|
22
|
+
console.log('[TabbySpaces] Registering commands:', commands)
|
|
23
|
+
for (const cmd of commands) {
|
|
24
|
+
this.pendingCommands.set(cmd.paneId, cmd)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private onTabOpened(tab: BaseTabComponent): void {
|
|
29
|
+
console.log('[TabbySpaces] Tab opened:', {
|
|
30
|
+
type: tab.constructor.name,
|
|
31
|
+
title: tab.title,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Handle SplitTabComponent - get all child terminal tabs
|
|
35
|
+
if (tab instanceof SplitTabComponent) {
|
|
36
|
+
console.log('[TabbySpaces] SplitTabComponent detected, waiting for children...')
|
|
37
|
+
// Wait for split tab to fully initialize its children
|
|
38
|
+
setTimeout(() => this.processChildTabs(tab), 300)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle individual terminal tab (shouldn't happen for split-layout, but just in case)
|
|
43
|
+
if (tab instanceof BaseTerminalTabComponent) {
|
|
44
|
+
this.processTerminalTab(tab)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private processChildTabs(splitTab: SplitTabComponent): void {
|
|
49
|
+
// Get all nested tabs from the split container
|
|
50
|
+
const allTabs = splitTab.getAllTabs()
|
|
51
|
+
console.log('[TabbySpaces] Found child tabs:', allTabs.length)
|
|
52
|
+
|
|
53
|
+
for (const tab of allTabs) {
|
|
54
|
+
if (tab instanceof BaseTerminalTabComponent) {
|
|
55
|
+
this.processTerminalTab(tab)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
private processTerminalTab(terminalTab: BaseTerminalTabComponent<any>): void {
|
|
62
|
+
const paneId = terminalTab.customTitle || terminalTab.title
|
|
63
|
+
console.log('[TabbySpaces] Processing terminal tab:', {
|
|
64
|
+
title: terminalTab.title,
|
|
65
|
+
customTitle: terminalTab.customTitle,
|
|
66
|
+
paneId,
|
|
67
|
+
pendingKeys: [...this.pendingCommands.keys()],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const pending = this.pendingCommands.get(paneId)
|
|
71
|
+
if (!pending) {
|
|
72
|
+
console.log('[TabbySpaces] No matching command for paneId:', paneId)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.pendingCommands.delete(paneId)
|
|
77
|
+
|
|
78
|
+
// Build startup command (cd + command)
|
|
79
|
+
const fullCommand = this.buildFullCommand(pending)
|
|
80
|
+
if (!fullCommand) {
|
|
81
|
+
console.log('[TabbySpaces] No command to send (no cwd or startup command)')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('[TabbySpaces] Command matched, waiting for shell output...:', fullCommand)
|
|
86
|
+
|
|
87
|
+
// Wait for shell to emit first output (prompt), then send command
|
|
88
|
+
if (terminalTab.session?.output$) {
|
|
89
|
+
terminalTab.session.output$.pipe(
|
|
90
|
+
first(), // Wait for first output (shell prompt)
|
|
91
|
+
switchMap(() => timer(100)) // Small buffer after prompt renders
|
|
92
|
+
).subscribe(() => {
|
|
93
|
+
console.log('[TabbySpaces] Shell ready, sending command:', fullCommand)
|
|
94
|
+
terminalTab.sendInput(fullCommand + '\r')
|
|
95
|
+
|
|
96
|
+
// Clear profile args to prevent native splits from re-running command
|
|
97
|
+
this.clearProfileArgs(terminalTab)
|
|
98
|
+
|
|
99
|
+
// Reset title - either to original or clear for dynamic shell title
|
|
100
|
+
if (pending.originalTitle) {
|
|
101
|
+
terminalTab.setTitle(pending.originalTitle)
|
|
102
|
+
} else {
|
|
103
|
+
terminalTab.customTitle = ''
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
} else {
|
|
107
|
+
console.log('[TabbySpaces] No session.output$, falling back to timeout')
|
|
108
|
+
// Fallback if session not available yet
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
terminalTab.sendInput(fullCommand + '\r')
|
|
111
|
+
|
|
112
|
+
// Clear profile args to prevent native splits from re-running command
|
|
113
|
+
this.clearProfileArgs(terminalTab)
|
|
114
|
+
|
|
115
|
+
if (pending.originalTitle) {
|
|
116
|
+
terminalTab.setTitle(pending.originalTitle)
|
|
117
|
+
} else {
|
|
118
|
+
terminalTab.customTitle = ''
|
|
119
|
+
}
|
|
120
|
+
}, 500)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private buildFullCommand(pending: PendingCommand): string | null {
|
|
125
|
+
return pending.command || null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
private clearProfileArgs(terminalTab: BaseTerminalTabComponent<any>): void {
|
|
130
|
+
// Clear args from profile to prevent native splits from re-running startup command
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
const profile = (terminalTab as any).profile
|
|
133
|
+
if (profile?.options?.args) {
|
|
134
|
+
console.log('[TabbySpaces] Clearing profile args to prevent re-run on split')
|
|
135
|
+
profile.options.args = []
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ngOnDestroy(): void {
|
|
140
|
+
this.subscription?.unsubscribe()
|
|
141
|
+
}
|
|
142
|
+
}
|