Files
ARIA-AGENT/android/src/services/codeFile.ts
T
duffyduckandClaude Opus 4.8 20c527c8ed feat: Workspace-Umbau Schritt 3 — Backend-Kanaele + code/desktop-Services
Verdrahtet die neuen Kanaele "dunkel" (noch ohne UI):
- rvs/server.js: ALLOWED_TYPES um code_file/code_file_edit, check_desktop/
  desktop_status und vnc_open/close/data/input erweitert (Base64-in-JSON-Relay
  wie audio_pcm, kein Binaer-Handling noetig).
- brainApi.ts: Project.kind ('code'|'chat') + desktop_url; ChatScreen publiziert
  die Kinds in den projectFocus-Spiegel.
- services/codeFile.ts: Live-Spiegel der Code-Dateien (content + Deltas) mit
  Ruckkanal code_file_edit fuer eigene Edits.
- services/desktop.ts: Desktop-Status + VNC-RFB-Tunnel (vnc_open/close/input/
  data) durch RVS, Session = Projekt-ID.

tsc clean, rvs/server.js syntaktisch OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:40:17 +02:00

136 lines
4.7 KiB
TypeScript

/**
* codeFile — Live-Spiegel der Code-Dateien, die ARIA in einem Code-Projekt
* schreibt, plus Ruckkanal fuer eigene Edits.
*
* Fluss: ARIAs Write/Edit (proxy) → Bridge → RVS `code_file` → hier gepuffert →
* CodeEditorTile zeigt es in CodeMirror. Tippt Stefan im Editor, geht das als
* `code_file_edit` ueber RVS zurueck an die Bridge (schreibt die Datei).
*
* Der Service haelt pro (projectId,path) den aktuellen Volltext, damit eine
* spaet gemountete Editor-Kachel sofort den Ist-Stand bekommt (setContent) und
* danach nur noch Deltas anwendet. Muster wie services/rvs.ts (Singleton).
*/
import rvs, { RVSMessage } from './rvs';
export interface CodePatch {
from: number;
to: number;
insert: string;
}
/** Persistenter Zustand einer Datei im Spiegel. */
export interface CodeFileState {
projectId: string;
path: string;
language: string;
content: string;
version: number;
}
/** Ein eingehendes Update — entweder Voll-Ersatz (content) oder Delta (patch). */
export interface CodeFileUpdate {
projectId: string;
path: string;
language: string;
version: number;
content?: string;
patch?: CodePatch;
}
type UpdateSub = (u: CodeFileUpdate) => void;
const SEP = '';
const keyOf = (projectId: string, path: string) => `${projectId || ''}${SEP}${path}`;
function applyPatch(content: string, p: CodePatch): string {
const from = Math.max(0, Math.min(p.from, content.length));
const to = Math.max(from, Math.min(p.to, content.length));
return content.slice(0, from) + (p.insert || '') + content.slice(to);
}
function guessLanguage(path: string): string {
const ext = (path.split('.').pop() || '').toLowerCase();
const map: Record<string, string> = {
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
ts: 'typescript', tsx: 'typescript',
py: 'python', rb: 'ruby', go: 'go', rs: 'rust',
c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp',
java: 'java', kt: 'kotlin', cs: 'csharp', php: 'php', swift: 'swift',
sh: 'shell', bash: 'shell', zsh: 'shell',
html: 'html', htm: 'html', xml: 'xml', css: 'css', scss: 'css',
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
md: 'markdown', sql: 'sql', bat: 'shell', asm: 'asm', s: 'asm',
};
return map[ext] || 'text';
}
class CodeFileService {
private files = new Map<string, CodeFileState>();
private subs: UpdateSub[] = [];
constructor() {
rvs.onMessage((m) => this.onMessage(m));
}
private onMessage(m: RVSMessage): void {
if (m.type !== 'code_file') return;
const p = (m.payload || {}) as any;
const projectId: string = p.projectId || '';
const path: string = p.path || '';
if (!path) return;
const language: string = p.language || guessLanguage(path);
const version: number = typeof p.version === 'number' ? p.version : 0;
const k = keyOf(projectId, path);
const prev = this.files.get(k);
let content: string;
let patch: CodePatch | undefined;
if (typeof p.content === 'string') {
content = p.content;
} else if (p.patch && typeof p.patch.from === 'number') {
patch = { from: p.patch.from, to: p.patch.to, insert: p.patch.insert || '' };
content = applyPatch(prev?.content || '', patch);
} else {
content = prev?.content || '';
}
this.files.set(k, { projectId, path, language, content, version });
const update: CodeFileUpdate = { projectId, path, language, version, content: p.content, patch };
this.subs.forEach((cb) => cb(update));
}
/** Alle bekannten Dateien eines Projekts (fuer die Datei-Liste in der Kachel). */
getFiles(projectId: string): CodeFileState[] {
const out: CodeFileState[] = [];
for (const f of this.files.values()) {
if ((f.projectId || '') === (projectId || '')) out.push(f);
}
return out.sort((a, b) => a.path.localeCompare(b.path));
}
getFile(projectId: string, path: string): CodeFileState | undefined {
return this.files.get(keyOf(projectId, path));
}
/** Registriert einen Listener fuer eingehende Updates. */
subscribe(cb: UpdateSub): () => void {
this.subs.push(cb);
return () => { this.subs = this.subs.filter((s) => s !== cb); };
}
/** Nutzer-Edit aus dem Editor zurueck an ARIA/Bridge. */
sendEdit(projectId: string, path: string, patch: CodePatch, fullText: string, version: number): void {
// Optimistisch den lokalen Spiegel nachziehen, damit ein direkt folgendes
// ARIA-Update nicht auf veraltetem Text patcht.
const k = keyOf(projectId, path);
const prev = this.files.get(k);
if (prev) this.files.set(k, { ...prev, content: fullText, version });
rvs.send('code_file_edit', { projectId, path, from: patch.from, to: patch.to, insert: patch.insert, fullText, version });
}
}
const codeFile = new CodeFileService();
export default codeFile;