tabby-tabbyspaces 0.1.0 → 0.2.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.
- package/.claude/settings.local.json +2 -1
- 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 +46 -0
- package/CLAUDE.md +33 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +21 -18
- package/TODO.md +5 -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 -18
- package/dist/components/paneEditor.component.d.ts.map +1 -1
- package/dist/components/splitPreview.component.d.ts +50 -50
- package/dist/components/splitPreview.component.d.ts.map +1 -1
- package/dist/components/workspaceEditor.component.d.ts +61 -54
- package/dist/components/workspaceEditor.component.d.ts.map +1 -1
- package/dist/components/workspaceList.component.d.ts +56 -39
- 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 -78
- package/dist/models/workspace.model.d.ts.map +1 -1
- package/dist/package.json +1 -1
- package/dist/providers/config.provider.d.ts +8 -8
- package/dist/providers/settings.provider.d.ts +7 -7
- package/dist/providers/toolbar.provider.d.ts +23 -15
- package/dist/providers/toolbar.provider.d.ts.map +1 -1
- package/dist/services/startupCommand.service.d.ts +27 -19
- package/dist/services/startupCommand.service.d.ts.map +1 -1
- 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 -32
- 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/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 +2 -1
- package/screenshots/editor.png +0 -0
- package/scripts/build-dev.js +2 -1
- package/scripts/build-prod.js +2 -1
- 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 +4 -32
- package/src/components/splitPreview.component.pug +0 -9
- package/src/components/splitPreview.component.scss +46 -70
- package/src/components/splitPreview.component.ts +15 -25
- package/src/components/workspaceEditor.component.pug +140 -112
- package/src/components/workspaceEditor.component.scss +270 -202
- package/src/components/workspaceEditor.component.ts +161 -85
- package/src/components/workspaceList.component.pug +31 -51
- package/src/components/workspaceList.component.scss +86 -77
- package/src/components/workspaceList.component.ts +89 -34
- package/src/index.ts +4 -0
- package/src/models/workspace.model.ts +80 -2
- package/src/providers/toolbar.provider.ts +78 -9
- package/src/services/startupCommand.service.ts +30 -32
- package/src/services/workspaceBackground.service.ts +167 -0
- package/src/services/workspaceEditor.service.ts +77 -40
- package/src/styles/_index.scss +3 -0
- package/src/styles/_mixins.scss +180 -0
- package/src/styles/_variables.scss +67 -0
- package/TEST_MCP.md +0 -176
- package/cdp-click.js +0 -22
- package/cdp-test.js +0 -28
- package/screenshots/pane-edit.png +0 -0
- package/test_cdp.py +0 -50
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core'
|
|
2
|
+
import { AppService, SplitTabComponent } from 'tabby-core'
|
|
3
|
+
import { WorkspaceEditorService } from './workspaceEditor.service'
|
|
4
|
+
import { WorkspaceBackground } from '../models/workspace.model'
|
|
5
|
+
import { CONFIG_KEY } from '../build-config'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Service for applying custom backgrounds to workspace tabs.
|
|
9
|
+
* Injects CSS dynamically based on workspace configuration.
|
|
10
|
+
*/
|
|
11
|
+
@Injectable({ providedIn: 'root' })
|
|
12
|
+
export class WorkspaceBackgroundService {
|
|
13
|
+
private styleElement: HTMLStyleElement | null = null
|
|
14
|
+
private appliedWorkspaces = new Map<string, string>() // workspaceId -> CSS
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private app: AppService,
|
|
18
|
+
private workspaceService: WorkspaceEditorService
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the service by setting up tab event listeners.
|
|
23
|
+
* Must be called once during app initialization.
|
|
24
|
+
*/
|
|
25
|
+
initialize(): void {
|
|
26
|
+
this.setupTabListeners()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private setupTabListeners(): void {
|
|
30
|
+
// Listen for tab open
|
|
31
|
+
this.app.tabOpened$.subscribe(tab => this.onTabOpened(tab))
|
|
32
|
+
|
|
33
|
+
// Listen for tab close - cleanup
|
|
34
|
+
this.app.tabClosed$.subscribe(tab => this.onTabClosed(tab))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private onTabOpened(tab: unknown): void {
|
|
38
|
+
if (!(tab instanceof SplitTabComponent)) return
|
|
39
|
+
|
|
40
|
+
// Small delay to let Angular finish rendering
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
const workspaceId = this.extractWorkspaceId(tab)
|
|
43
|
+
if (!workspaceId) return
|
|
44
|
+
|
|
45
|
+
const workspace = this.workspaceService.getWorkspaces()
|
|
46
|
+
.find(w => w.id === workspaceId)
|
|
47
|
+
|
|
48
|
+
if (workspace?.background && workspace.background.type !== 'none') {
|
|
49
|
+
this.applyBackground(workspaceId, workspace.background)
|
|
50
|
+
}
|
|
51
|
+
}, 200)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private onTabClosed(tab: unknown): void {
|
|
55
|
+
if (!(tab instanceof SplitTabComponent)) return
|
|
56
|
+
|
|
57
|
+
const workspaceId = this.extractWorkspaceId(tab)
|
|
58
|
+
if (workspaceId) {
|
|
59
|
+
this.removeBackground(workspaceId)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract workspace ID from a SplitTabComponent.
|
|
65
|
+
* Tries multiple strategies: _recoveredState and child profile ID.
|
|
66
|
+
*/
|
|
67
|
+
private extractWorkspaceId(tab: SplitTabComponent): string | undefined {
|
|
68
|
+
const tabAny = tab as any
|
|
69
|
+
|
|
70
|
+
// Strategy 1: Check _recoveredState.workspaceId (for restored tabs)
|
|
71
|
+
if (tabAny._recoveredState?.workspaceId) {
|
|
72
|
+
return tabAny._recoveredState.workspaceId
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Strategy 2: Extract from child profile ID (for freshly opened tabs)
|
|
76
|
+
const profilePrefix = `split-layout:${CONFIG_KEY}:`
|
|
77
|
+
for (const child of tab.getAllTabs()) {
|
|
78
|
+
const profileId = (child as any).profile?.id ?? ''
|
|
79
|
+
if (profileId.startsWith(profilePrefix)) {
|
|
80
|
+
// Profile ID format: split-layout:CONFIG_KEY:name:UUID
|
|
81
|
+
const parts = profileId.split(':')
|
|
82
|
+
return parts[parts.length - 1]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private applyBackground(workspaceId: string, bg: WorkspaceBackground): void {
|
|
90
|
+
// Mark split-tab element with data attribute
|
|
91
|
+
this.markSplitTabElement(workspaceId)
|
|
92
|
+
|
|
93
|
+
// Generate and inject CSS
|
|
94
|
+
const css = this.generateCSS(workspaceId, bg)
|
|
95
|
+
this.injectCSS(workspaceId, css)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private markSplitTabElement(workspaceId: string): void {
|
|
99
|
+
// Find split-tab that doesn't have a workspace-id yet
|
|
100
|
+
const splitTabs = document.querySelectorAll('split-tab')
|
|
101
|
+
for (let i = splitTabs.length - 1; i >= 0; i--) {
|
|
102
|
+
const splitTab = splitTabs[i]
|
|
103
|
+
if (!splitTab.hasAttribute('data-workspace-id')) {
|
|
104
|
+
splitTab.setAttribute('data-workspace-id', workspaceId)
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private generateCSS(workspaceId: string, bg: WorkspaceBackground): string {
|
|
111
|
+
if (bg.type === 'none' || !bg.value) return ''
|
|
112
|
+
|
|
113
|
+
return `
|
|
114
|
+
split-tab[data-workspace-id="${workspaceId}"] {
|
|
115
|
+
background: ${bg.value} !important;
|
|
116
|
+
}
|
|
117
|
+
split-tab[data-workspace-id="${workspaceId}"] .xterm-viewport,
|
|
118
|
+
split-tab[data-workspace-id="${workspaceId}"] .xterm-screen {
|
|
119
|
+
background: transparent !important;
|
|
120
|
+
}
|
|
121
|
+
`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private injectCSS(workspaceId: string, css: string): void {
|
|
125
|
+
if (!this.styleElement) {
|
|
126
|
+
this.styleElement = document.createElement('style')
|
|
127
|
+
this.styleElement.id = 'tabbyspaces-backgrounds'
|
|
128
|
+
document.head.appendChild(this.styleElement)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.appliedWorkspaces.set(workspaceId, css)
|
|
132
|
+
this.updateStyleElement()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private removeBackground(workspaceId: string): void {
|
|
136
|
+
this.appliedWorkspaces.delete(workspaceId)
|
|
137
|
+
this.updateStyleElement()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private updateStyleElement(): void {
|
|
141
|
+
if (this.styleElement) {
|
|
142
|
+
this.styleElement.textContent = Array.from(this.appliedWorkspaces.values()).join('\n')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Refresh background for a specific workspace.
|
|
148
|
+
* Call this when workspace background is updated in settings.
|
|
149
|
+
*/
|
|
150
|
+
refreshWorkspaceBackground(workspaceId: string): void {
|
|
151
|
+
const workspace = this.workspaceService.getWorkspaces()
|
|
152
|
+
.find(w => w.id === workspaceId)
|
|
153
|
+
|
|
154
|
+
if (!workspace) {
|
|
155
|
+
this.removeBackground(workspaceId)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (workspace.background && workspace.background.type !== 'none') {
|
|
160
|
+
const css = this.generateCSS(workspaceId, workspace.background)
|
|
161
|
+
this.appliedWorkspaces.set(workspaceId, css)
|
|
162
|
+
} else {
|
|
163
|
+
this.appliedWorkspaces.delete(workspaceId)
|
|
164
|
+
}
|
|
165
|
+
this.updateStyleElement()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
WorkspaceSplit,
|
|
7
7
|
isWorkspaceSplit,
|
|
8
8
|
generateUUID,
|
|
9
|
+
deepClone,
|
|
9
10
|
TabbyProfile,
|
|
10
11
|
TabbyRecoveryToken,
|
|
11
12
|
TabbySplitLayoutProfile,
|
|
@@ -15,7 +16,9 @@ import { PendingCommand } from './startupCommand.service'
|
|
|
15
16
|
|
|
16
17
|
@Injectable({ providedIn: 'root' })
|
|
17
18
|
export class WorkspaceEditorService {
|
|
18
|
-
private cachedProfiles: TabbyProfile[] =
|
|
19
|
+
private cachedProfiles: TabbyProfile[] | null = null
|
|
20
|
+
private cacheTimestamp: number = 0
|
|
21
|
+
private readonly CACHE_TTL = 30000 // 30 seconds
|
|
19
22
|
|
|
20
23
|
constructor(
|
|
21
24
|
private config: ConfigService,
|
|
@@ -23,49 +26,78 @@ export class WorkspaceEditorService {
|
|
|
23
26
|
private profilesService: ProfilesService
|
|
24
27
|
) {}
|
|
25
28
|
|
|
26
|
-
private async
|
|
27
|
-
|
|
29
|
+
private async getCachedProfiles(): Promise<TabbyProfile[]> {
|
|
30
|
+
const now = Date.now()
|
|
31
|
+
if (!this.cachedProfiles || now - this.cacheTimestamp > this.CACHE_TTL) {
|
|
32
|
+
this.cachedProfiles = (await this.profilesService.getProfiles()) as TabbyProfile[]
|
|
33
|
+
this.cacheTimestamp = now
|
|
34
|
+
}
|
|
35
|
+
return this.cachedProfiles
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
/** Returns all saved workspaces from config. */
|
|
30
39
|
getWorkspaces(): Workspace[] {
|
|
31
40
|
return this.config.store?.[CONFIG_KEY]?.workspaces ?? []
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Saves the workspace list to config.
|
|
45
|
+
* @throws Error if config store is not initialized
|
|
46
|
+
*/
|
|
47
|
+
async saveWorkspaces(workspaces: Workspace[]): Promise<void> {
|
|
35
48
|
if (!this.config.store?.[CONFIG_KEY]) {
|
|
36
|
-
|
|
49
|
+
throw new Error('Config store not initialized')
|
|
37
50
|
}
|
|
38
51
|
this.config.store[CONFIG_KEY].workspaces = workspaces
|
|
39
|
-
|
|
52
|
+
await this.saveConfig()
|
|
40
53
|
}
|
|
41
54
|
|
|
55
|
+
/** Adds a new workspace and shows notification. */
|
|
42
56
|
async addWorkspace(workspace: Workspace): Promise<void> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
try {
|
|
58
|
+
const workspaces = this.getWorkspaces()
|
|
59
|
+
workspaces.push(workspace)
|
|
60
|
+
await this.saveWorkspaces(workspaces)
|
|
61
|
+
this.notifications.info(`Workspace "${workspace.name}" created`)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.notifications.error(`Failed to create workspace "${workspace.name}"`)
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
47
66
|
}
|
|
48
67
|
|
|
68
|
+
/** Updates an existing workspace by ID and shows notification. */
|
|
49
69
|
async updateWorkspace(workspace: Workspace): Promise<void> {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
try {
|
|
71
|
+
const workspaces = this.getWorkspaces()
|
|
72
|
+
const index = workspaces.findIndex((w) => w.id === workspace.id)
|
|
73
|
+
if (index !== -1) {
|
|
74
|
+
workspaces[index] = workspace
|
|
75
|
+
await this.saveWorkspaces(workspaces)
|
|
76
|
+
this.notifications.info(`Workspace "${workspace.name}" updated`)
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.notifications.error(`Failed to update workspace "${workspace.name}"`)
|
|
80
|
+
throw error
|
|
56
81
|
}
|
|
57
82
|
}
|
|
58
83
|
|
|
84
|
+
/** Deletes a workspace by ID and shows notification. */
|
|
59
85
|
async deleteWorkspace(workspaceId: string): Promise<void> {
|
|
60
86
|
const workspaces = this.getWorkspaces()
|
|
61
87
|
const workspace = workspaces.find((w) => w.id === workspaceId)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
try {
|
|
89
|
+
const filtered = workspaces.filter((w) => w.id !== workspaceId)
|
|
90
|
+
await this.saveWorkspaces(filtered)
|
|
91
|
+
if (workspace) {
|
|
92
|
+
this.notifications.info(`Workspace "${workspace.name}" deleted`)
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.notifications.error(`Failed to delete workspace "${workspace?.name || workspaceId}"`)
|
|
96
|
+
throw error
|
|
66
97
|
}
|
|
67
98
|
}
|
|
68
99
|
|
|
100
|
+
/** Returns all local shell profiles available for use in workspaces. */
|
|
69
101
|
async getAvailableProfiles(): Promise<TabbyProfile[]> {
|
|
70
102
|
const allProfiles = await this.profilesService.getProfiles()
|
|
71
103
|
return allProfiles.filter(
|
|
@@ -93,8 +125,9 @@ export class WorkspaceEditorService {
|
|
|
93
125
|
}
|
|
94
126
|
}
|
|
95
127
|
|
|
128
|
+
/** Generates a Tabby split-layout profile from a workspace for opening. */
|
|
96
129
|
async generateTabbyProfile(workspace: Workspace): Promise<TabbySplitLayoutProfile> {
|
|
97
|
-
await this.
|
|
130
|
+
await this.getCachedProfiles()
|
|
98
131
|
const safeName = this.sanitizeForProfileId(workspace.name)
|
|
99
132
|
return {
|
|
100
133
|
id: `split-layout:${CONFIG_KEY}:${safeName}:${workspace.id}`,
|
|
@@ -105,26 +138,27 @@ export class WorkspaceEditorService {
|
|
|
105
138
|
color: workspace.color,
|
|
106
139
|
isBuiltin: false,
|
|
107
140
|
options: {
|
|
108
|
-
recoveryToken: this.generateRecoveryToken(workspace.root),
|
|
141
|
+
recoveryToken: this.generateRecoveryToken(workspace.root, workspace.name, workspace.id),
|
|
109
142
|
},
|
|
110
143
|
}
|
|
111
144
|
}
|
|
112
145
|
|
|
113
|
-
private generateRecoveryToken(split: WorkspaceSplit): TabbyRecoveryToken {
|
|
146
|
+
private generateRecoveryToken(split: WorkspaceSplit, workspaceName: string, workspaceId: string): TabbyRecoveryToken {
|
|
114
147
|
return {
|
|
115
148
|
type: 'app:split-tab',
|
|
116
149
|
orientation: split.orientation === 'horizontal' ? 'h' : 'v',
|
|
117
150
|
ratios: split.ratios,
|
|
151
|
+
workspaceId,
|
|
118
152
|
children: split.children.map((child) => {
|
|
119
153
|
if (isWorkspaceSplit(child)) {
|
|
120
|
-
return this.generateRecoveryToken(child)
|
|
154
|
+
return this.generateRecoveryToken(child, workspaceName, workspaceId)
|
|
121
155
|
}
|
|
122
|
-
return this.generatePaneToken(child)
|
|
156
|
+
return this.generatePaneToken(child, workspaceName, workspaceId)
|
|
123
157
|
}),
|
|
124
158
|
}
|
|
125
159
|
}
|
|
126
160
|
|
|
127
|
-
private generatePaneToken(pane: WorkspacePane): TabbyRecoveryToken {
|
|
161
|
+
private generatePaneToken(pane: WorkspacePane, workspaceName: string, workspaceId: string): TabbyRecoveryToken {
|
|
128
162
|
const baseProfile = this.getProfileById(pane.profileId)
|
|
129
163
|
|
|
130
164
|
if (!baseProfile) {
|
|
@@ -162,7 +196,7 @@ export class WorkspaceEditorService {
|
|
|
162
196
|
options,
|
|
163
197
|
icon: baseProfile.icon || '',
|
|
164
198
|
color: baseProfile.color || '',
|
|
165
|
-
disableDynamicTitle:
|
|
199
|
+
disableDynamicTitle: true,
|
|
166
200
|
weight: 0,
|
|
167
201
|
isBuiltin: false,
|
|
168
202
|
isTemplate: false,
|
|
@@ -170,22 +204,25 @@ export class WorkspaceEditorService {
|
|
|
170
204
|
behaviorOnSessionEnd: 'auto',
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
//
|
|
174
|
-
//
|
|
207
|
+
// tabTitle: workspace name (what user sees)
|
|
208
|
+
// tabCustomTitle: pane.id (for matching in StartupCommandService)
|
|
209
|
+
// workspaceId: for duplicate detection after Tabby recovery
|
|
175
210
|
const cwd = pane.cwd || baseProfile.options?.cwd || ''
|
|
176
211
|
return {
|
|
177
212
|
type: 'app:local-tab',
|
|
178
213
|
profile,
|
|
179
214
|
savedState: false,
|
|
180
|
-
tabTitle:
|
|
215
|
+
tabTitle: workspaceName,
|
|
181
216
|
tabCustomTitle: pane.id,
|
|
182
|
-
|
|
217
|
+
workspaceId,
|
|
218
|
+
disableDynamicTitle: true,
|
|
183
219
|
cwd,
|
|
184
220
|
}
|
|
185
221
|
}
|
|
186
222
|
|
|
223
|
+
/** Creates a deep copy of a workspace with new IDs. */
|
|
187
224
|
duplicateWorkspace(workspace: Workspace): Workspace {
|
|
188
|
-
const clone =
|
|
225
|
+
const clone = deepClone(workspace)
|
|
189
226
|
clone.id = generateUUID()
|
|
190
227
|
clone.name = `${workspace.name} (Copy)`
|
|
191
228
|
clone.launchOnStartup = false
|
|
@@ -221,40 +258,40 @@ export class WorkspaceEditorService {
|
|
|
221
258
|
if (found) return found
|
|
222
259
|
|
|
223
260
|
// Fallback: check cached profiles (includes built-ins)
|
|
224
|
-
return this.cachedProfiles
|
|
261
|
+
return this.cachedProfiles?.find((p) => p.id === profileId && isLocalType(p.type))
|
|
225
262
|
}
|
|
226
263
|
|
|
264
|
+
/** Collects all startup commands from panes in a workspace. */
|
|
227
265
|
collectStartupCommands(workspace: Workspace): PendingCommand[] {
|
|
228
266
|
const commands: PendingCommand[] = []
|
|
229
|
-
this.collectCommandsFromNode(workspace.root, commands)
|
|
267
|
+
this.collectCommandsFromNode(workspace.root, workspace.name, commands)
|
|
230
268
|
return commands
|
|
231
269
|
}
|
|
232
270
|
|
|
233
271
|
private collectCommandsFromNode(
|
|
234
272
|
node: WorkspacePane | WorkspaceSplit,
|
|
273
|
+
workspaceName: string,
|
|
235
274
|
commands: PendingCommand[]
|
|
236
275
|
): void {
|
|
237
276
|
if (isWorkspaceSplit(node)) {
|
|
238
277
|
for (const child of node.children) {
|
|
239
|
-
this.collectCommandsFromNode(child, commands)
|
|
278
|
+
this.collectCommandsFromNode(child, workspaceName, commands)
|
|
240
279
|
}
|
|
241
280
|
} else if (node.startupCommand) {
|
|
242
281
|
commands.push({
|
|
243
282
|
paneId: node.id,
|
|
244
283
|
command: node.startupCommand,
|
|
245
|
-
originalTitle:
|
|
284
|
+
originalTitle: workspaceName,
|
|
246
285
|
})
|
|
247
286
|
}
|
|
248
287
|
}
|
|
249
288
|
|
|
250
|
-
private async saveConfig(): Promise<
|
|
289
|
+
private async saveConfig(): Promise<void> {
|
|
251
290
|
try {
|
|
252
291
|
await this.config.save()
|
|
253
|
-
return true
|
|
254
292
|
} catch (error) {
|
|
255
|
-
this.notifications.error('Failed to save configuration')
|
|
256
293
|
console.error('TabbySpaces save error:', error)
|
|
257
|
-
|
|
294
|
+
throw error
|
|
258
295
|
}
|
|
259
296
|
}
|
|
260
297
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
@use 'variables' as *;
|
|
2
|
+
|
|
3
|
+
// ======================
|
|
4
|
+
// LAYOUT MIXINS
|
|
5
|
+
// ======================
|
|
6
|
+
|
|
7
|
+
@mixin flex-center {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ======================
|
|
14
|
+
// FORM INPUT MIXIN
|
|
15
|
+
// ======================
|
|
16
|
+
|
|
17
|
+
@mixin form-input($bg: var(--theme-bg)) {
|
|
18
|
+
padding: $spacing-sm $spacing-md;
|
|
19
|
+
border-radius: $radius-sm;
|
|
20
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
21
|
+
background: $bg;
|
|
22
|
+
color: var(--theme-fg);
|
|
23
|
+
font-size: $font-md;
|
|
24
|
+
|
|
25
|
+
&:focus {
|
|
26
|
+
outline: none;
|
|
27
|
+
border-color: var(--theme-primary);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&::placeholder {
|
|
31
|
+
color: var(--theme-fg-more, $fallback-fg-more);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ======================
|
|
36
|
+
// FORM LABEL MIXIN (S1: Uppercase compact)
|
|
37
|
+
// ======================
|
|
38
|
+
|
|
39
|
+
@mixin form-label {
|
|
40
|
+
display: block;
|
|
41
|
+
font-size: $font-xs;
|
|
42
|
+
color: var(--theme-fg-less);
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.5px;
|
|
45
|
+
margin-bottom: $spacing-xs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ======================
|
|
49
|
+
// BUTTON MIXINS
|
|
50
|
+
// ======================
|
|
51
|
+
|
|
52
|
+
@mixin toolbar-btn {
|
|
53
|
+
padding: $spacing-sm $spacing-md;
|
|
54
|
+
background: var(--theme-bg);
|
|
55
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
56
|
+
border-radius: $radius-sm;
|
|
57
|
+
color: var(--theme-fg);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
transition: all $transition-fast;
|
|
60
|
+
font-size: 0.85rem;
|
|
61
|
+
|
|
62
|
+
&:hover:not(:disabled) {
|
|
63
|
+
background: var(--theme-bg-more-more);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:disabled {
|
|
67
|
+
opacity: 0.4;
|
|
68
|
+
cursor: not-allowed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&.danger {
|
|
72
|
+
color: var(--theme-danger, $color-danger);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@mixin btn-success {
|
|
77
|
+
background: $color-success;
|
|
78
|
+
border-color: $color-success;
|
|
79
|
+
color: white;
|
|
80
|
+
|
|
81
|
+
&:hover {
|
|
82
|
+
background: $color-success-hover;
|
|
83
|
+
border-color: $color-success-hover;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@mixin btn-base {
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: $spacing-sm;
|
|
91
|
+
padding: $spacing-sm $spacing-lg;
|
|
92
|
+
font-size: $font-sm;
|
|
93
|
+
border-radius: $radius-sm;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
transition: all $transition-fast;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@mixin btn-ghost {
|
|
99
|
+
@include btn-base;
|
|
100
|
+
background: transparent;
|
|
101
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
102
|
+
color: var(--theme-fg);
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
background: var(--theme-bg-more);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@mixin btn-primary {
|
|
110
|
+
@include btn-base;
|
|
111
|
+
background: var(--theme-primary);
|
|
112
|
+
border: 1px solid var(--theme-primary);
|
|
113
|
+
color: white;
|
|
114
|
+
|
|
115
|
+
&:hover {
|
|
116
|
+
filter: brightness(1.1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@mixin icon-btn-sm($size: 24px) {
|
|
121
|
+
width: $size;
|
|
122
|
+
height: $size;
|
|
123
|
+
@include flex-center;
|
|
124
|
+
background: var(--theme-bg);
|
|
125
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
126
|
+
border-radius: $radius-sm;
|
|
127
|
+
color: var(--theme-fg-more, $fallback-fg-more);
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
transition: all $transition-fast;
|
|
131
|
+
|
|
132
|
+
&:hover:not(:disabled) {
|
|
133
|
+
border-color: var(--theme-primary);
|
|
134
|
+
color: var(--theme-primary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
&.danger:hover:not(:disabled) {
|
|
138
|
+
border-color: var(--theme-danger);
|
|
139
|
+
color: var(--theme-danger);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
&:disabled {
|
|
143
|
+
opacity: 0.4;
|
|
144
|
+
cursor: not-allowed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ======================
|
|
149
|
+
// OVERLAY/MODAL MIXINS
|
|
150
|
+
// ======================
|
|
151
|
+
|
|
152
|
+
@mixin full-overlay($z-index: $z-modal-overlay) {
|
|
153
|
+
position: fixed;
|
|
154
|
+
top: 0;
|
|
155
|
+
left: 0;
|
|
156
|
+
right: 0;
|
|
157
|
+
bottom: 0;
|
|
158
|
+
z-index: $z-index;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ======================
|
|
162
|
+
// TEXT UTILITIES
|
|
163
|
+
// ======================
|
|
164
|
+
|
|
165
|
+
@mixin text-ellipsis {
|
|
166
|
+
white-space: nowrap;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
text-overflow: ellipsis;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ======================
|
|
172
|
+
// DROPDOWN/POPUP
|
|
173
|
+
// ======================
|
|
174
|
+
|
|
175
|
+
@mixin dropdown-panel {
|
|
176
|
+
background: var(--theme-bg-more);
|
|
177
|
+
border: 1px solid var(--theme-border, $fallback-border);
|
|
178
|
+
border-radius: $radius-md;
|
|
179
|
+
box-shadow: $shadow-dropdown;
|
|
180
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ======================
|
|
2
|
+
// SPACING SCALE (S1: Tight)
|
|
3
|
+
// ======================
|
|
4
|
+
$spacing-xs: 2px; // toolbar group gap, tiny margins
|
|
5
|
+
$spacing-sm: 4px; // gaps, small padding
|
|
6
|
+
$spacing-md: 6px; // default gaps, button padding
|
|
7
|
+
$spacing-lg: 10px; // section gaps, form spacing
|
|
8
|
+
$spacing-xl: 14px; // container padding, large gaps
|
|
9
|
+
$spacing-2xl: 18px; // modal padding, section margins
|
|
10
|
+
|
|
11
|
+
// ======================
|
|
12
|
+
// BORDER RADIUS SCALE (S1: Sharp)
|
|
13
|
+
// ======================
|
|
14
|
+
$radius-xs: 2px; // tiny elements
|
|
15
|
+
$radius-sm: 2px; // buttons, small elements
|
|
16
|
+
$radius-md: 3px; // inputs, dropdowns
|
|
17
|
+
$radius-lg: 4px; // cards, containers, modals
|
|
18
|
+
|
|
19
|
+
// ======================
|
|
20
|
+
// CUSTOM COLORS
|
|
21
|
+
// ======================
|
|
22
|
+
// These extend Tabby's --theme-* variables
|
|
23
|
+
$color-success: #10b981;
|
|
24
|
+
$color-success-hover: #059669;
|
|
25
|
+
$color-warning: #f59e0b;
|
|
26
|
+
$color-danger: #ef4444;
|
|
27
|
+
|
|
28
|
+
// Fallback values for Tabby theme variables that may not be defined
|
|
29
|
+
$fallback-border: rgba(255, 255, 255, 0.1);
|
|
30
|
+
$fallback-fg-more: rgba(255, 255, 255, 0.3);
|
|
31
|
+
|
|
32
|
+
// ======================
|
|
33
|
+
// SHADOWS (S1: Flat - no shadows)
|
|
34
|
+
// ======================
|
|
35
|
+
$shadow-dropdown: none;
|
|
36
|
+
$shadow-context-menu: none;
|
|
37
|
+
$shadow-modal: none;
|
|
38
|
+
$overlay-bg: rgba(0, 0, 0, 0.7);
|
|
39
|
+
|
|
40
|
+
// ======================
|
|
41
|
+
// PREVIEW COMPONENT
|
|
42
|
+
// ======================
|
|
43
|
+
$preview-height: 140px;
|
|
44
|
+
$nested-split-bg: rgba(255, 255, 255, 0.02);
|
|
45
|
+
$selected-pane-gradient-start: rgba(59, 130, 246, 0.08);
|
|
46
|
+
$selected-pane-gradient-end: rgba(59, 130, 246, 0.04);
|
|
47
|
+
|
|
48
|
+
// ======================
|
|
49
|
+
// FONT SIZES (S1: Compact)
|
|
50
|
+
// ======================
|
|
51
|
+
$font-xs: 0.7rem; // 11px - sublabels, hints
|
|
52
|
+
$font-sm: 0.8rem; // 13px - labels, secondary text
|
|
53
|
+
$font-md: 0.85rem; // 14px - default body
|
|
54
|
+
|
|
55
|
+
// ======================
|
|
56
|
+
// Z-INDEX SCALE
|
|
57
|
+
// ======================
|
|
58
|
+
$z-dropdown: 100;
|
|
59
|
+
$z-modal-overlay: 1100;
|
|
60
|
+
$z-context-menu-overlay: 1200;
|
|
61
|
+
$z-context-menu: 1201;
|
|
62
|
+
|
|
63
|
+
// ======================
|
|
64
|
+
// TRANSITIONS
|
|
65
|
+
// ======================
|
|
66
|
+
$transition-fast: 0.15s;
|
|
67
|
+
$transition-normal: 0.2s;
|