@theia/ai-claude-code 1.67.0-next.59 → 1.67.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 (28) hide show
  1. package/lib/browser/claude-code-chat-agent.d.ts +8 -3
  2. package/lib/browser/claude-code-chat-agent.d.ts.map +1 -1
  3. package/lib/browser/claude-code-chat-agent.js +23 -10
  4. package/lib/browser/claude-code-chat-agent.js.map +1 -1
  5. package/lib/browser/claude-code-edit-tool-service.d.ts +2 -0
  6. package/lib/browser/claude-code-edit-tool-service.d.ts.map +1 -1
  7. package/lib/browser/claude-code-edit-tool-service.js +10 -5
  8. package/lib/browser/claude-code-edit-tool-service.js.map +1 -1
  9. package/lib/browser/claude-code-file-edit-backup-service.d.ts +2 -0
  10. package/lib/browser/claude-code-file-edit-backup-service.d.ts.map +1 -1
  11. package/lib/browser/claude-code-file-edit-backup-service.js +7 -1
  12. package/lib/browser/claude-code-file-edit-backup-service.js.map +1 -1
  13. package/lib/browser/claude-code-slash-commands-contribution.d.ts +24 -7
  14. package/lib/browser/claude-code-slash-commands-contribution.d.ts.map +1 -1
  15. package/lib/browser/claude-code-slash-commands-contribution.js +121 -57
  16. package/lib/browser/claude-code-slash-commands-contribution.js.map +1 -1
  17. package/lib/browser/renderers/web-fetch-tool-renderer.js +1 -1
  18. package/lib/browser/renderers/web-fetch-tool-renderer.js.map +1 -1
  19. package/lib/common/claude-code-service.d.ts +1 -0
  20. package/lib/common/claude-code-service.d.ts.map +1 -1
  21. package/lib/common/claude-code-service.js.map +1 -1
  22. package/package.json +11 -11
  23. package/src/browser/claude-code-chat-agent.ts +26 -12
  24. package/src/browser/claude-code-edit-tool-service.ts +10 -7
  25. package/src/browser/claude-code-file-edit-backup-service.ts +6 -2
  26. package/src/browser/claude-code-slash-commands-contribution.ts +152 -73
  27. package/src/browser/renderers/web-fetch-tool-renderer.tsx +1 -1
  28. package/src/common/claude-code-service.ts +1 -0
@@ -14,17 +14,18 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { CHAT_VIEW_LANGUAGE_ID } from '@theia/ai-chat-ui/lib/browser/chat-view-language-contribution';
18
- import { nls, URI } from '@theia/core';
17
+ import { PromptService } from '@theia/ai-core/lib/common/prompt-service';
18
+ import { DisposableCollection, ILogger, nls, URI } from '@theia/core';
19
19
  import { FrontendApplicationContribution } from '@theia/core/lib/browser';
20
- import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
21
- import { inject, injectable } from '@theia/core/shared/inversify';
20
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
22
21
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
23
- import * as monaco from '@theia/monaco-editor-core';
22
+ import { FileChangeType } from '@theia/filesystem/lib/common/files';
24
23
  import { WorkspaceService } from '@theia/workspace/lib/browser';
25
24
  import { CLAUDE_CHAT_AGENT_ID } from './claude-code-chat-agent';
26
25
 
27
26
  const CLAUDE_COMMANDS = '.claude/commands';
27
+ const COMMAND_FRAGMENT_PREFIX = 'claude-code-slash-';
28
+ const DYNAMIC_COMMAND_PREFIX = 'claude-code-dynamic-';
28
29
 
29
30
  interface StaticSlashCommand {
30
31
  name: string;
@@ -65,8 +66,11 @@ export class ClaudeCodeSlashCommandsContribution implements FrontendApplicationC
65
66
  }
66
67
  ];
67
68
 
68
- @inject(ContextKeyService)
69
- protected readonly contextKeyService: ContextKeyService;
69
+ @inject(ILogger) @named('claude-code')
70
+ protected readonly logger: ILogger;
71
+
72
+ @inject(PromptService)
73
+ protected readonly promptService: PromptService;
70
74
 
71
75
  @inject(WorkspaceService)
72
76
  protected readonly workspaceService: WorkspaceService;
@@ -74,86 +78,161 @@ export class ClaudeCodeSlashCommandsContribution implements FrontendApplicationC
74
78
  @inject(FileService)
75
79
  protected readonly fileService: FileService;
76
80
 
77
- onStart(): void {
78
- monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
79
- triggerCharacters: ['/'],
80
- provideCompletionItems: (model, position, _context, _token) =>
81
- this.provideSlashCompletions(model, position),
82
- });
83
- }
84
-
85
- protected async provideSlashCompletions(
86
- model: monaco.editor.ITextModel,
87
- position: monaco.Position
88
- ): Promise<monaco.languages.CompletionList> {
89
- const isClaudeCode = this.contextKeyService.match(`chatInputReceivingAgent == '${CLAUDE_CHAT_AGENT_ID}'`);
90
- if (!isClaudeCode) {
91
- return { suggestions: [] };
81
+ protected readonly toDispose = new DisposableCollection();
82
+ protected currentWorkspaceRoot: URI | undefined;
83
+ protected fileWatcherDisposable: DisposableCollection | undefined;
84
+
85
+ async onStart(): Promise<void> {
86
+ this.registerStaticCommands();
87
+ await this.initializeDynamicCommands();
88
+
89
+ this.toDispose.push(
90
+ this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChange())
91
+ );
92
+ }
93
+
94
+ onStop(): void {
95
+ this.toDispose.dispose();
96
+ }
97
+
98
+ protected registerStaticCommands(): void {
99
+ for (const command of this.staticCommands) {
100
+ this.promptService.addBuiltInPromptFragment({
101
+ id: `${COMMAND_FRAGMENT_PREFIX}${command.name}`,
102
+ template: `/${command.name}`,
103
+ isCommand: true,
104
+ commandName: command.name,
105
+ commandDescription: command.description,
106
+ commandAgents: [CLAUDE_CHAT_AGENT_ID]
107
+ });
92
108
  }
109
+ }
93
110
 
94
- const completionRange = this.getCompletionRange(model, position, '/');
95
- if (completionRange === undefined) {
96
- return { suggestions: [] };
111
+ protected async initializeDynamicCommands(): Promise<void> {
112
+ const workspaceRoot = this.getWorkspaceRoot();
113
+ if (!workspaceRoot) {
114
+ return;
97
115
  }
98
116
 
117
+ this.currentWorkspaceRoot = workspaceRoot;
118
+ await this.registerDynamicCommandsForWorkspace(workspaceRoot);
119
+ this.setupFileWatcher(workspaceRoot);
120
+ }
121
+
122
+ protected async registerDynamicCommandsForWorkspace(workspaceRoot: URI): Promise<void> {
123
+ const commandsUri = this.getCommandsUri(workspaceRoot);
124
+ const files = await this.listMarkdownFiles(commandsUri);
125
+
126
+ for (const filename of files) {
127
+ await this.registerDynamicCommand(commandsUri, filename);
128
+ }
129
+ }
130
+
131
+ protected async registerDynamicCommand(commandsDir: URI, filename: string): Promise<void> {
132
+ const commandName = this.getCommandNameFromFilename(filename);
133
+ const fileUri = commandsDir.resolve(filename);
134
+
99
135
  try {
100
- const suggestions: monaco.languages.CompletionItem[] = [];
101
-
102
- // Add static commands
103
- this.staticCommands.forEach(command => {
104
- suggestions.push({
105
- insertText: `${command.name} `,
106
- kind: monaco.languages.CompletionItemKind.Function,
107
- label: command.name,
108
- range: completionRange,
109
- detail: command.description
110
- });
136
+ const content = await this.fileService.read(fileUri);
137
+ this.promptService.addBuiltInPromptFragment({
138
+ id: this.getDynamicCommandId(commandName),
139
+ template: content.value,
140
+ isCommand: true,
141
+ commandName,
142
+ commandAgents: [CLAUDE_CHAT_AGENT_ID]
111
143
  });
144
+ } catch (error) {
145
+ this.logger.error(`Failed to register Claude Code slash command '${commandName}' from ${fileUri}:`, error);
146
+ }
147
+ }
112
148
 
113
- // Add dynamic commands from .claude/commands directory
114
- const roots = this.workspaceService.tryGetRoots();
115
- if (roots.length >= 1) {
116
- const uri = roots[0].resource;
117
- const claudeCommandsUri = uri.resolve(CLAUDE_COMMANDS);
118
- const files = await this.listFilesDirectly(claudeCommandsUri);
119
- const commands = files
120
- .filter(file => file.endsWith('.md'))
121
- .map(file => file.replace(/\.md$/, ''));
122
-
123
- commands.forEach(commandName => {
124
- suggestions.push({
125
- insertText: `${commandName} `,
126
- kind: monaco.languages.CompletionItemKind.Function,
127
- label: commandName,
128
- range: completionRange,
129
- detail: nls.localize('theia/ai/claude-code/commandDetail', 'Claude command: {0}', commandName)
130
- });
131
- });
132
- }
149
+ protected setupFileWatcher(workspaceRoot: URI): void {
150
+ this.fileWatcherDisposable?.dispose();
151
+ this.fileWatcherDisposable = new DisposableCollection();
133
152
 
134
- return { suggestions };
135
- } catch (error) {
136
- console.error('Error in Claude completion provider:', error);
137
- return { suggestions: [] };
153
+ const commandsUri = this.getCommandsUri(workspaceRoot);
154
+
155
+ this.fileWatcherDisposable.push(
156
+ this.fileService.onDidFilesChange(async event => {
157
+ const relevantChanges = event.changes.filter(change =>
158
+ this.isCommandFile(change.resource, commandsUri)
159
+ );
160
+
161
+ if (relevantChanges.length === 0) {
162
+ return;
163
+ }
164
+
165
+ for (const change of relevantChanges) {
166
+ await this.handleFileChange(change.resource, change.type, commandsUri);
167
+ }
168
+ })
169
+ );
170
+
171
+ this.toDispose.push(this.fileWatcherDisposable);
172
+ }
173
+
174
+ protected async handleFileChange(resource: URI, changeType: FileChangeType, commandsUri: URI): Promise<void> {
175
+ const filename = resource.path.base;
176
+ const commandName = this.getCommandNameFromFilename(filename);
177
+ const fragmentId = this.getDynamicCommandId(commandName);
178
+
179
+ if (changeType === FileChangeType.DELETED) {
180
+ this.promptService.removePromptFragment(fragmentId);
181
+ } else if (changeType === FileChangeType.ADDED || changeType === FileChangeType.UPDATED) {
182
+ await this.registerDynamicCommand(commandsUri, filename);
138
183
  }
139
184
  }
140
185
 
141
- protected getCompletionRange(model: monaco.editor.ITextModel, position: monaco.Position, triggerCharacter: string): monaco.Range | undefined {
142
- const wordInfo = model.getWordUntilPosition(position);
143
- const lineContent = model.getLineContent(position.lineNumber);
186
+ protected async handleWorkspaceChange(): Promise<void> {
187
+ const newRoot = this.getWorkspaceRoot();
144
188
 
145
- // one to the left, and -1 for 0-based index
146
- const characterBeforeCurrentWord = lineContent[wordInfo.startColumn - 1 - 1];
147
- if (characterBeforeCurrentWord !== triggerCharacter) {
148
- return undefined;
189
+ if (this.currentWorkspaceRoot?.toString() === newRoot?.toString()) {
190
+ return;
149
191
  }
150
192
 
151
- return new monaco.Range(
152
- position.lineNumber,
153
- wordInfo.startColumn,
154
- position.lineNumber,
155
- position.column
156
- );
193
+ await this.clearDynamicCommands();
194
+ this.currentWorkspaceRoot = newRoot;
195
+ await this.initializeDynamicCommands();
196
+ }
197
+
198
+ protected async clearDynamicCommands(): Promise<void> {
199
+ if (!this.currentWorkspaceRoot) {
200
+ return;
201
+ }
202
+
203
+ const commandsUri = this.getCommandsUri(this.currentWorkspaceRoot);
204
+ const files = await this.listMarkdownFiles(commandsUri);
205
+
206
+ for (const filename of files) {
207
+ const commandName = this.getCommandNameFromFilename(filename);
208
+ this.promptService.removePromptFragment(this.getDynamicCommandId(commandName));
209
+ }
210
+ }
211
+
212
+ protected getWorkspaceRoot(): URI | undefined {
213
+ const roots = this.workspaceService.tryGetRoots();
214
+ return roots.length > 0 ? roots[0].resource : undefined;
215
+ }
216
+
217
+ protected getCommandsUri(workspaceRoot: URI): URI {
218
+ return workspaceRoot.resolve(CLAUDE_COMMANDS);
219
+ }
220
+
221
+ protected isCommandFile(resource: URI, commandsUri: URI): boolean {
222
+ return resource.toString().startsWith(commandsUri.toString()) && resource.path.ext === '.md';
223
+ }
224
+
225
+ protected getCommandNameFromFilename(filename: string): string {
226
+ return filename.replace(/\.md$/, '');
227
+ }
228
+
229
+ protected getDynamicCommandId(commandName: string): string {
230
+ return `${DYNAMIC_COMMAND_PREFIX}${commandName}`;
231
+ }
232
+
233
+ protected async listMarkdownFiles(uri: URI): Promise<string[]> {
234
+ const allFiles = await this.listFilesDirectly(uri);
235
+ return allFiles.filter(file => file.endsWith('.md'));
157
236
  }
158
237
 
159
238
  protected async listFilesDirectly(uri: URI): Promise<string[]> {
@@ -86,7 +86,7 @@ const WebFetchToolComponent: React.FC<{
86
86
  const expandedContent = (
87
87
  <div className="claude-code-tool details">
88
88
  <div className="claude-code-tool detail-row">
89
- <span className="claude-code-tool detail-label">{nls.localize('theia/ai/claude-code/url', 'URL')}</span>
89
+ <span className="claude-code-tool detail-label">{nls.localizeByDefault('URL')}</span>
90
90
  <code className="claude-code-tool detail-value">{input.url}</code>
91
91
  </div>
92
92
  <div className="claude-code-tool detail-row">
@@ -237,6 +237,7 @@ export interface ClaudeCodeOptions {
237
237
  executableArgs?: string[];
238
238
  extraArgs?: Record<string, string | null>;
239
239
  fallbackModel?: string;
240
+ forkSession?: boolean;
240
241
  maxThinkingTokens?: number;
241
242
  maxTurns?: number;
242
243
  model?: string;