OXYGENHUB
# 95a01bce821d spectra-vscode / src / briefs.ts
3.2 KiB · last changed Spectra for VS Code 0.1: set up and chat without the command line (#6) 1 day ago source blame history raw
1
// Briefs as documents (spec §5.2): `spectra-brief:/overall.md` and
2
// `spectra-brief:/project/NAME.md`. Opening one reads it from Spectra;
3
// saving it records a signed brief.set, so agents are told.
4
import * as vscode from "vscode";
5
import type { Briefs } from "./spectra-api";
6
import { Bridge } from "./bridge";
7
8
export const SCHEME = "spectra-brief";
9
10
export function uriFor(project?: string): vscode.Uri {
11
  return vscode.Uri.from({ scheme: SCHEME, path: project ? `/project/${project}.md` : "/overall.md" });
12
}
13
14
function projectOf(uri: vscode.Uri): string | undefined {
15
  const m = /^\/project\/([a-z0-9][a-z0-9-]*)\.md$/.exec(uri.path);
16
  if (m) return m[1];
17
  if (uri.path === "/overall.md") return undefined;
18
  throw vscode.FileSystemError.FileNotFound(uri);
19
}
20
21
export class BriefFs implements vscode.FileSystemProvider {
22
  private readonly emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
23
  readonly onDidChangeFile = this.emitter.event;
24
  private readonly times = new Map<string, number>();
25
26
  constructor(private readonly bridge: Bridge) {}
27
28
  watch(): vscode.Disposable {
29
    return new vscode.Disposable(() => undefined);
30
  }
31
32
  /** Tell open editors that briefs changed in Spectra. */
33
  changed(): void {
34
    const now = Date.now();
35
    const events: vscode.FileChangeEvent[] = [];
36
    for (const k of this.times.keys()) {
37
      this.times.set(k, now);
38
      events.push({ type: vscode.FileChangeType.Changed, uri: vscode.Uri.parse(k) });
39
    }
40
    if (events.length) this.emitter.fire(events);
41
  }
42
43
  private async text(uri: vscode.Uri): Promise<string> {
44
    const project = projectOf(uri);
45
    const b = await this.bridge.request<Briefs>("GET", "/api/v1/briefs");
46
    return (project ? b.projects.find((p) => p.project === project)?.text : b.overall) ?? "";
47
  }
48
49
  async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
50
    projectOf(uri);
51
    const key = uri.toString();
52
    if (!this.times.has(key)) this.times.set(key, Date.now());
53
    const size = Buffer.byteLength(await this.text(uri).catch(() => ""));
54
    return { type: vscode.FileType.File, ctime: 0, mtime: this.times.get(key)!, size };
55
  }
56
57
  async readFile(uri: vscode.Uri): Promise<Uint8Array> {
58
    return Buffer.from(await this.text(uri));
59
  }
60
61
  async writeFile(uri: vscode.Uri, content: Uint8Array): Promise<void> {
62
    const project = projectOf(uri);
63
    try {
64
      await this.bridge.request("PUT", "/api/v1/briefs", { project: project ?? null, text: Buffer.from(content).toString("utf8") });
65
    } catch (e) {
66
      throw vscode.FileSystemError.NoPermissions(`Spectra did not take the brief: ${(e as Error).message}`);
67
    }
68
    this.times.set(uri.toString(), Date.now());
69
    void vscode.window.setStatusBarMessage(`$(check) ${project ? `${project}'s brief` : "Overall brief"} signed into Spectra; agents are told on their next call`, 5000);
70
  }
71
72
  readDirectory(): [string, vscode.FileType][] {
73
    return [];
74
  }
75
  createDirectory(): void {
76
    throw vscode.FileSystemError.NoPermissions("briefs are not folders");
77
  }
78
  delete(): void {
79
    throw vscode.FileSystemError.NoPermissions("clear a brief by saving it empty");
80
  }
81
  rename(): void {
82
    throw vscode.FileSystemError.NoPermissions("briefs cannot be renamed");
83
  }
84
}