@kispace-io/extension-howto-system 0.8.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/package.json +19 -0
- package/src/README.md +249 -0
- package/src/contributions/ai-setup-howto-contributions.ts +205 -0
- package/src/contributions/onboarding-howto-contributions.ts +196 -0
- package/src/howto-contribution.ts +98 -0
- package/src/howto-extension.ts +107 -0
- package/src/howto-service.ts +123 -0
- package/src/i18n.json +11 -0
- package/src/index.ts +16 -0
- package/src/k-howto-panel.ts +1058 -0
- package/tsconfig.json +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kispace-io/extension-howto-system",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@kispace-io/core": "*",
|
|
14
|
+
"@kispace-io/extension-ai-system": "*"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.9.3"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# HowTo System Extension
|
|
2
|
+
|
|
3
|
+
The HowTo System provides a framework for creating step-by-step workflows that guide users through specific processes. It features a floating, draggable UI panel that displays workflows with pre and post condition checks.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Register HowToContributions**: Extensions can register workflows with multiple steps
|
|
8
|
+
- **Floating UI Panel**: Always-on-top, draggable panel that hosts the workflow steps
|
|
9
|
+
- **Sequential Step Execution**: Steps are executed one at a time in order
|
|
10
|
+
- **Pre and Post Conditions**: Each step can have conditions that check requirements and outcomes
|
|
11
|
+
- **Step Status Tracking**: Visual indicators show step status (pending, active, completed, failed, skipped)
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Registering a HowTo Contribution
|
|
16
|
+
|
|
17
|
+
From any extension, you can register a HowTo contribution via the contribution registry:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { contributionRegistry } from '../../core/contributionregistry';
|
|
21
|
+
import { HOWTO_CONTRIBUTION_TARGET } from '../../extensions/howto-system/howto-extension';
|
|
22
|
+
import type { HowToContribution } from '../../extensions/howto-system/howto-extension';
|
|
23
|
+
|
|
24
|
+
export default function myExtension({ contributionRegistry }: any) {
|
|
25
|
+
// Register a HowTo contribution
|
|
26
|
+
contributionRegistry.registerContribution<HowToContribution>(HOWTO_CONTRIBUTION_TARGET, {
|
|
27
|
+
id: 'my-extension.setup-workspace',
|
|
28
|
+
title: 'Setup Workspace',
|
|
29
|
+
description: 'Guide to set up your workspace for the first time',
|
|
30
|
+
icon: 'folder-open',
|
|
31
|
+
label: 'Setup Workspace', // Required by Contribution interface
|
|
32
|
+
steps: [
|
|
33
|
+
{
|
|
34
|
+
id: 'step-1',
|
|
35
|
+
title: 'Create Project Folder',
|
|
36
|
+
description: 'Create a new folder for your project',
|
|
37
|
+
preCondition: async () => {
|
|
38
|
+
// Check if workspace is selected
|
|
39
|
+
const workspace = await workspaceService.getWorkspace();
|
|
40
|
+
return workspace !== undefined;
|
|
41
|
+
},
|
|
42
|
+
postCondition: async () => {
|
|
43
|
+
// Check if project folder exists
|
|
44
|
+
const workspace = await workspaceService.getWorkspace();
|
|
45
|
+
if (!workspace) return false;
|
|
46
|
+
const projectFolder = await workspace.getChild('my-project');
|
|
47
|
+
return projectFolder !== undefined;
|
|
48
|
+
},
|
|
49
|
+
command: 'workspace.create-folder',
|
|
50
|
+
commandParams: { name: 'my-project' }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'step-2',
|
|
54
|
+
title: 'Initialize Configuration',
|
|
55
|
+
description: 'Create a configuration file',
|
|
56
|
+
preCondition: async () => {
|
|
57
|
+
// Previous step must be completed
|
|
58
|
+
return true; // This will be checked automatically
|
|
59
|
+
},
|
|
60
|
+
postCondition: async () => {
|
|
61
|
+
const workspace = await workspaceService.getWorkspace();
|
|
62
|
+
if (!workspace) return false;
|
|
63
|
+
const configFile = await workspace.getChild('config.json');
|
|
64
|
+
return configFile !== undefined;
|
|
65
|
+
},
|
|
66
|
+
command: 'workspace.create-file',
|
|
67
|
+
commandParams: { name: 'config.json', content: '{}' }
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using Conditions
|
|
75
|
+
|
|
76
|
+
Conditions are functions that return a boolean or Promise<boolean>:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Simple synchronous condition
|
|
80
|
+
preCondition: () => {
|
|
81
|
+
return someVariable === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Async condition
|
|
85
|
+
preCondition: async () => {
|
|
86
|
+
const result = await someAsyncCheck();
|
|
87
|
+
return result.isValid;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Condition with error handling
|
|
91
|
+
postCondition: async () => {
|
|
92
|
+
try {
|
|
93
|
+
const file = await workspaceService.getFile('path/to/file');
|
|
94
|
+
return file !== undefined;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Step Commands
|
|
102
|
+
|
|
103
|
+
Steps can optionally execute commands when activated:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
{
|
|
107
|
+
id: 'step-1',
|
|
108
|
+
title: 'Open Editor',
|
|
109
|
+
description: 'Open the code editor',
|
|
110
|
+
command: 'editor.open',
|
|
111
|
+
commandParams: {
|
|
112
|
+
file: 'src/main.ts'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Optional Steps
|
|
118
|
+
|
|
119
|
+
Steps can be marked as optional, allowing users to skip them:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
{
|
|
123
|
+
id: 'step-3',
|
|
124
|
+
title: 'Install Dependencies (Optional)',
|
|
125
|
+
description: 'Install optional dependencies',
|
|
126
|
+
optional: true,
|
|
127
|
+
// ...
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## HowTo Panel UI
|
|
132
|
+
|
|
133
|
+
The floating panel provides:
|
|
134
|
+
|
|
135
|
+
- **Workflow List**: Shows all registered workflows
|
|
136
|
+
- **Step List**: Displays all steps in the active workflow
|
|
137
|
+
- **Status Indicators**: Visual feedback for each step's status
|
|
138
|
+
- **Condition Checks**: Shows whether pre/post conditions are met
|
|
139
|
+
- **Action Buttons**: Execute or skip steps
|
|
140
|
+
- **Drag to Move**: Click and drag the header to reposition
|
|
141
|
+
|
|
142
|
+
### Showing the Panel
|
|
143
|
+
|
|
144
|
+
The panel is hidden by default. You can show it using:
|
|
145
|
+
|
|
146
|
+
1. **Toolbar Button**: Click the "HowTo" button in the bottom right toolbar
|
|
147
|
+
2. **Keyboard Shortcut**: Press `Ctrl+Shift+H` to toggle the panel
|
|
148
|
+
3. **Command**: Execute the `howto.show-panel` or `howto.toggle-panel` command
|
|
149
|
+
4. **Command Palette**: Search for "Show HowTo Panel" or "Toggle HowTo Panel"
|
|
150
|
+
|
|
151
|
+
The panel visibility state is persisted in localStorage, so it will remember whether it was visible when you reload the page.
|
|
152
|
+
|
|
153
|
+
## API Reference
|
|
154
|
+
|
|
155
|
+
### HowToService
|
|
156
|
+
|
|
157
|
+
The HowToService manages HowTo contributions by reading from the contribution registry:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
class HowToService {
|
|
161
|
+
// Get a contribution by ID
|
|
162
|
+
getContribution(contributionId: string): HowToContribution | undefined;
|
|
163
|
+
|
|
164
|
+
// Get all contributions
|
|
165
|
+
getAllContributions(): HowToContribution[];
|
|
166
|
+
|
|
167
|
+
// Get contributions by category
|
|
168
|
+
getContributionsByCategory(category: string): HowToContribution[];
|
|
169
|
+
|
|
170
|
+
// Check if a contribution exists
|
|
171
|
+
hasContribution(contributionId: string): boolean;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The service is registered in the dependency injection context and can be accessed via:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
export default function myExtension({ howToService }: any) {
|
|
179
|
+
const contributions = howToService.getAllContributions();
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or imported directly:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { howToService } from '../../extensions/howto-system/howto-extension';
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### HowToContribution
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface HowToContribution {
|
|
193
|
+
id: string;
|
|
194
|
+
title: string;
|
|
195
|
+
description?: string;
|
|
196
|
+
icon?: string;
|
|
197
|
+
category?: string;
|
|
198
|
+
steps: HowToStep[];
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### HowToStep
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
interface HowToStep {
|
|
206
|
+
id: string;
|
|
207
|
+
title: string;
|
|
208
|
+
description: string;
|
|
209
|
+
preCondition?: ConditionFunction;
|
|
210
|
+
postCondition?: ConditionFunction;
|
|
211
|
+
command?: string;
|
|
212
|
+
commandParams?: Record<string, any>;
|
|
213
|
+
optional?: boolean;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### ConditionFunction
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
type ConditionFunction = () => Promise<boolean> | boolean;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Events
|
|
224
|
+
|
|
225
|
+
The system uses the contribution registry's events. Subscribe to contribution changes:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { TOPIC_CONTRIBUTEIONS_CHANGED } from '../../core/contributionregistry';
|
|
229
|
+
import { HOWTO_CONTRIBUTION_TARGET } from '../../extensions/howto-system/howto-extension';
|
|
230
|
+
import { subscribe } from '../../core/events';
|
|
231
|
+
|
|
232
|
+
subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event) => {
|
|
233
|
+
if (event.target === HOWTO_CONTRIBUTION_TARGET) {
|
|
234
|
+
console.log('HowTo contributions changed:', event.contributions);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Contribution Target
|
|
240
|
+
|
|
241
|
+
All HowTo contributions must be registered to the target:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { HOWTO_CONTRIBUTION_TARGET } from '../../extensions/howto-system/howto-extension';
|
|
245
|
+
|
|
246
|
+
// Use this constant when registering contributions
|
|
247
|
+
contributionRegistry.registerContribution(HOWTO_CONTRIBUTION_TARGET, contribution);
|
|
248
|
+
```
|
|
249
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { contributionRegistry, activeEditorSignal, partDirtySignal, subscribe, appLoaderService, appSettings } from '@kispace-io/core';
|
|
2
|
+
import type { EditorContentProvider } from '@kispace-io/core';
|
|
3
|
+
import { watchSignal } from '@kispace-io/core';
|
|
4
|
+
import { HOWTO_CONTRIBUTION_TARGET } from '../howto-service';
|
|
5
|
+
import type { HowToContribution, HowToContext } from '../howto-contribution';
|
|
6
|
+
import { KEY_AI_CONFIG, TOPIC_AICONFIG_CHANGED } from '@kispace-io/extension-ai-system';
|
|
7
|
+
import type { AIConfig } from '@kispace-io/extension-ai-system';
|
|
8
|
+
|
|
9
|
+
const AI_CONFIG_EDITOR_KEY = '.system.ai-config';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if an editor implements EditorContentProvider
|
|
13
|
+
*/
|
|
14
|
+
function isEditorContentProvider(editor: any): editor is EditorContentProvider {
|
|
15
|
+
return editor &&
|
|
16
|
+
typeof editor.getFilePath === 'function';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if the AI config editor is open
|
|
21
|
+
*/
|
|
22
|
+
function isAIConfigEditorOpen(): boolean {
|
|
23
|
+
const activeEditor = activeEditorSignal.get();
|
|
24
|
+
if (!activeEditor || !isEditorContentProvider(activeEditor)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const filePath = activeEditor.getFilePath();
|
|
29
|
+
return filePath === AI_CONFIG_EDITOR_KEY;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Checks if an LLM provider is configured (has default provider with API key)
|
|
34
|
+
*/
|
|
35
|
+
async function isLLMProviderConfigured(): Promise<boolean> {
|
|
36
|
+
try {
|
|
37
|
+
const config = await appSettings.get(KEY_AI_CONFIG) as AIConfig | undefined;
|
|
38
|
+
if (!config || !config.defaultProvider) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const defaultProvider = config.providers?.find(p => p.name === config.defaultProvider);
|
|
43
|
+
if (!defaultProvider) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if API key is set (not empty)
|
|
48
|
+
return !!defaultProvider.apiKey && defaultProvider.apiKey.trim() !== '';
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if the AI config editor has unsaved changes
|
|
56
|
+
*/
|
|
57
|
+
function isAIConfigEditorDirty(): boolean {
|
|
58
|
+
const activeEditor = activeEditorSignal.get();
|
|
59
|
+
if (!activeEditor || !isEditorContentProvider(activeEditor)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filePath = activeEditor.getFilePath();
|
|
64
|
+
if (filePath !== AI_CONFIG_EDITOR_KEY) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return activeEditor.isDirty() === true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if the AI config editor is saved (not dirty)
|
|
73
|
+
*/
|
|
74
|
+
function isAIConfigEditorSaved(): boolean {
|
|
75
|
+
if (!isAIConfigEditorOpen()) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return !isAIConfigEditorDirty();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks if the AI config editor is closed
|
|
84
|
+
*/
|
|
85
|
+
function isAIConfigEditorClosed(): boolean {
|
|
86
|
+
return !isAIConfigEditorOpen();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if user has typed something in the AI chat
|
|
91
|
+
* This checks if there are any chat sessions with messages
|
|
92
|
+
*/
|
|
93
|
+
async function hasTypedInChat(): Promise<boolean> {
|
|
94
|
+
try {
|
|
95
|
+
const sessions = await appSettings.get('aiChatSessions') as any;
|
|
96
|
+
if (!sessions || typeof sessions !== 'object') {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if any session has messages
|
|
101
|
+
for (const sessionId in sessions) {
|
|
102
|
+
const session = sessions[sessionId];
|
|
103
|
+
if (session?.history && Array.isArray(session.history)) {
|
|
104
|
+
// Check if there's at least one user message
|
|
105
|
+
const hasUserMessage = session.history.some((msg: any) =>
|
|
106
|
+
msg.role === 'user' && msg.content && msg.content.trim() !== ''
|
|
107
|
+
);
|
|
108
|
+
if (hasUserMessage) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get current app name for the title
|
|
121
|
+
function getAppName(): string {
|
|
122
|
+
const currentApp = appLoaderService.getCurrentApp();
|
|
123
|
+
return currentApp?.name || 'AppSpace';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Create the AI setup HowTo contribution
|
|
127
|
+
const aiSetupContribution: HowToContribution = {
|
|
128
|
+
id: 'appspace.ai-setup',
|
|
129
|
+
title: () => `Set up AI in ${getAppName()}`,
|
|
130
|
+
description: () => `Configure an LLM provider to enable AI chat features in ${getAppName()}`,
|
|
131
|
+
icon: 'robot',
|
|
132
|
+
label: '',
|
|
133
|
+
category: 'Getting Started',
|
|
134
|
+
initialize: (context: HowToContext) => {
|
|
135
|
+
// Set up subscriptions for editor and AI config changes
|
|
136
|
+
const cleanups: (() => void)[] = [];
|
|
137
|
+
|
|
138
|
+
// Watch editor changes
|
|
139
|
+
cleanups.push(
|
|
140
|
+
watchSignal(activeEditorSignal, () => {
|
|
141
|
+
context.requestUpdate();
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Watch dirty state changes
|
|
146
|
+
cleanups.push(
|
|
147
|
+
watchSignal(partDirtySignal, () => {
|
|
148
|
+
context.requestUpdate();
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Subscribe to AI config changes
|
|
153
|
+
subscribe(TOPIC_AICONFIG_CHANGED, () => {
|
|
154
|
+
context.requestUpdate();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Return cleanup function
|
|
158
|
+
return () => {
|
|
159
|
+
cleanups.forEach(cleanup => cleanup());
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
steps: [
|
|
163
|
+
{
|
|
164
|
+
id: 'open-ai-settings',
|
|
165
|
+
title: 'Open AI Settings',
|
|
166
|
+
description: 'Open the AI settings editor by clicking the robot icon in the toolbar or using the command palette.',
|
|
167
|
+
preCondition: () => true, // Always available
|
|
168
|
+
postCondition: () => isAIConfigEditorOpen(),
|
|
169
|
+
command: 'open_ai_config',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'configure-llm-provider',
|
|
173
|
+
title: 'Configure LLM Provider',
|
|
174
|
+
description: 'Select a provider as default and enter an API key. Make sure to save your changes using Ctrl+S or the save button.',
|
|
175
|
+
preCondition: () => isAIConfigEditorOpen(),
|
|
176
|
+
postCondition: async () => {
|
|
177
|
+
// Check if provider is configured AND settings are saved
|
|
178
|
+
const configured = await isLLMProviderConfigured();
|
|
179
|
+
const saved = isAIConfigEditorSaved();
|
|
180
|
+
return configured && saved;
|
|
181
|
+
},
|
|
182
|
+
// No command - user manually configures in the editor
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'save-and-close',
|
|
186
|
+
title: 'Save and Close',
|
|
187
|
+
description: 'Save your changes (if not already saved) and close the AI settings editor tab.',
|
|
188
|
+
preCondition: () => isAIConfigEditorOpen(),
|
|
189
|
+
postCondition: () => isAIConfigEditorClosed(),
|
|
190
|
+
// No command - user manually saves and closes the tab
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'type-in-chat',
|
|
194
|
+
title: 'Type in Chat',
|
|
195
|
+
description: 'Open the AI chat view (if not already open) and type a message to test your AI configuration.',
|
|
196
|
+
preCondition: async () => await isLLMProviderConfigured(),
|
|
197
|
+
postCondition: async () => await hasTypedInChat(),
|
|
198
|
+
// No command - user manually types in chat
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Register the contribution
|
|
204
|
+
contributionRegistry.registerContribution<HowToContribution>(HOWTO_CONTRIBUTION_TARGET, aiSetupContribution);
|
|
205
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { contributionRegistry, workspaceService, File, TOPIC_WORKSPACE_CHANGED, TOPIC_WORKSPACE_CONNECTED, activeEditorSignal, partDirtySignal, subscribe, appLoaderService } from '@kispace-io/core';
|
|
2
|
+
import type { EditorContentProvider } from '@kispace-io/core';
|
|
3
|
+
import { watchSignal } from '@kispace-io/core';
|
|
4
|
+
import { HOWTO_CONTRIBUTION_TARGET } from '../howto-service';
|
|
5
|
+
import type { HowToContribution, HowToContext } from '../howto-contribution';
|
|
6
|
+
|
|
7
|
+
const ONBOARDING_FILE_PATH = 'welcome.txt';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type guard to check if an editor implements EditorContentProvider
|
|
11
|
+
*/
|
|
12
|
+
function isEditorContentProvider(editor: any): editor is EditorContentProvider {
|
|
13
|
+
return editor &&
|
|
14
|
+
typeof editor.getFilePath === 'function';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a workspace is selected
|
|
19
|
+
*/
|
|
20
|
+
async function isWorkspaceSelected(): Promise<boolean> {
|
|
21
|
+
return workspaceService.isConnected();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if the onboarding file exists
|
|
26
|
+
*/
|
|
27
|
+
async function onboardingFileExists(): Promise<boolean> {
|
|
28
|
+
const workspace = await workspaceService.getWorkspace();
|
|
29
|
+
if (!workspace) return false;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const resource = await workspace.getResource(ONBOARDING_FILE_PATH);
|
|
33
|
+
return resource instanceof File;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks if the onboarding file is open in an editor
|
|
41
|
+
*/
|
|
42
|
+
function isOnboardingFileOpen(): boolean {
|
|
43
|
+
const activeEditor = activeEditorSignal.get();
|
|
44
|
+
if (!activeEditor || !isEditorContentProvider(activeEditor)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const filePath = activeEditor.getFilePath();
|
|
49
|
+
return filePath === ONBOARDING_FILE_PATH;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if the active editor is dirty (has unsaved changes)
|
|
54
|
+
* Returns true only if the onboarding file is open AND dirty
|
|
55
|
+
*/
|
|
56
|
+
function isActiveEditorDirty(): boolean {
|
|
57
|
+
if (!isOnboardingFileOpen()) return false;
|
|
58
|
+
|
|
59
|
+
const activeEditor = activeEditorSignal.get();
|
|
60
|
+
if (!activeEditor) return false;
|
|
61
|
+
|
|
62
|
+
return activeEditor.isDirty() === true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks if the active editor is clean (no unsaved changes)
|
|
67
|
+
* Returns true only if the onboarding file is open AND not dirty
|
|
68
|
+
*/
|
|
69
|
+
function isActiveEditorClean(): boolean {
|
|
70
|
+
if (!isOnboardingFileOpen()) return false;
|
|
71
|
+
|
|
72
|
+
const activeEditor = activeEditorSignal.get();
|
|
73
|
+
if (!activeEditor) return false;
|
|
74
|
+
|
|
75
|
+
return activeEditor.isDirty() === false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if the onboarding file is closed (not open in any editor)
|
|
80
|
+
*/
|
|
81
|
+
function isOnboardingFileClosed(): boolean {
|
|
82
|
+
return !isOnboardingFileOpen();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get current app name for the title
|
|
86
|
+
function getAppName(): string {
|
|
87
|
+
const currentApp = appLoaderService.getCurrentApp();
|
|
88
|
+
return currentApp?.name || 'AppSpace';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create the onboarding HowTo contribution
|
|
92
|
+
// Using callback functions so the app name is read when the HowTo is displayed
|
|
93
|
+
const onboardingContribution: HowToContribution = {
|
|
94
|
+
id: 'appspace.onboarding',
|
|
95
|
+
title: () => `Welcome to ${getAppName()}`,
|
|
96
|
+
description: () => `Get started with ${getAppName()} by learning the basics of workspace and file management`,
|
|
97
|
+
icon: 'graduation-cap',
|
|
98
|
+
// label will be set from title in howto-service.ts
|
|
99
|
+
label: '',
|
|
100
|
+
category: 'Getting Started',
|
|
101
|
+
initialize: (context: HowToContext) => {
|
|
102
|
+
// Set up subscriptions for workspace and editor changes
|
|
103
|
+
const cleanups: (() => void)[] = [];
|
|
104
|
+
|
|
105
|
+
// Subscribe to workspace events
|
|
106
|
+
subscribe(TOPIC_WORKSPACE_CHANGED, () => {
|
|
107
|
+
context.requestUpdate();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
subscribe(TOPIC_WORKSPACE_CONNECTED, () => {
|
|
111
|
+
context.requestUpdate();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Watch editor signals
|
|
115
|
+
cleanups.push(
|
|
116
|
+
watchSignal(activeEditorSignal, () => {
|
|
117
|
+
context.requestUpdate();
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
cleanups.push(
|
|
122
|
+
watchSignal(partDirtySignal, () => {
|
|
123
|
+
context.requestUpdate();
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Return cleanup function
|
|
128
|
+
return () => {
|
|
129
|
+
cleanups.forEach(cleanup => cleanup());
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
steps: [
|
|
133
|
+
{
|
|
134
|
+
id: 'create-text-file',
|
|
135
|
+
title: 'Create welcome.txt',
|
|
136
|
+
description: 'Create a new text file called "welcome.txt" in your workspace. If you don\'t have a workspace selected, choose one first.',
|
|
137
|
+
preCondition: async () => {
|
|
138
|
+
// Workspace must be selected
|
|
139
|
+
return await isWorkspaceSelected();
|
|
140
|
+
},
|
|
141
|
+
postCondition: async () => {
|
|
142
|
+
return await onboardingFileExists();
|
|
143
|
+
},
|
|
144
|
+
command: 'create_file',
|
|
145
|
+
commandParams: {
|
|
146
|
+
path: ONBOARDING_FILE_PATH,
|
|
147
|
+
contents: 'Welcome to AppSpace!\n\nThis is your first file. You can edit it and save your changes.'
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'open-text-file',
|
|
152
|
+
title: 'Open welcome.txt',
|
|
153
|
+
description: 'Open the "welcome.txt" file in the editor.',
|
|
154
|
+
preCondition: async () => {
|
|
155
|
+
return await onboardingFileExists();
|
|
156
|
+
},
|
|
157
|
+
postCondition: () => {
|
|
158
|
+
return isOnboardingFileOpen();
|
|
159
|
+
},
|
|
160
|
+
command: 'open_editor',
|
|
161
|
+
commandParams: {
|
|
162
|
+
path: ONBOARDING_FILE_PATH
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'edit-and-save',
|
|
167
|
+
title: 'Type something and save',
|
|
168
|
+
description: 'Type some text in the editor to modify the file, then save it using Ctrl+S or the save button.',
|
|
169
|
+
preCondition: () => {
|
|
170
|
+
return isOnboardingFileOpen();
|
|
171
|
+
},
|
|
172
|
+
postCondition: () => {
|
|
173
|
+
// File must be open, was dirty (edited), and is now clean (saved)
|
|
174
|
+
// Check that file is open and clean (saved)
|
|
175
|
+
return isActiveEditorClean();
|
|
176
|
+
},
|
|
177
|
+
// No command - user manually edits and saves
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'close-text-file',
|
|
181
|
+
title: 'Close the file',
|
|
182
|
+
description: 'Close the editor tab by clicking the X button on the tab.',
|
|
183
|
+
preCondition: () => {
|
|
184
|
+
return isOnboardingFileOpen();
|
|
185
|
+
},
|
|
186
|
+
postCondition: () => {
|
|
187
|
+
return isOnboardingFileClosed();
|
|
188
|
+
},
|
|
189
|
+
// No command - user manually closes the tab
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Register the contribution
|
|
195
|
+
contributionRegistry.registerContribution<HowToContribution>(HOWTO_CONTRIBUTION_TARGET, onboardingContribution);
|
|
196
|
+
|