OXYGENHUB
◆ main spectra-vscode / src / model.ts
2.7 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
// What the side bar shows, kept current from the event stream.
2
import * as vscode from "vscode";
3
import type { Agent, Brief, Briefs, Channel, InboxItem, Project } from "./spectra-api";
4
import { Bridge } from "./bridge";
5
import { projectForFolder } from "./pure";
6
7
export interface StreamEvent {
8
  seq: number;
9
  kind: string;
10
  actor: string;
11
  subject?: string | null;
12
}
13
14
export class Model implements vscode.Disposable {
15
  brief: Brief | undefined;
16
  agents: Agent[] = [];
17
  channels: Channel[] = [];
18
  projects: Project[] = [];
19
  inbox: InboxItem[] = [];
20
  briefs: Briefs = { overall: null, projects: [] };
21
  error: string | undefined;
22
23
  private readonly changed = new vscode.EventEmitter<void>();
24
  readonly onDidChange = this.changed.event;
25
  private readonly events = new vscode.EventEmitter<StreamEvent>();
26
  /** Every event from the stream, for open chats. */
27
  readonly onEvent = this.events.event;
28
  private timer: NodeJS.Timeout | undefined;
29
  private stream: vscode.Disposable | undefined;
30
31
  constructor(private readonly bridge: Bridge) {}
32
33
  /** Start following the stream and fetch everything. */
34
  start(): void {
35
    this.stream?.dispose();
36
    this.stream = this.bridge.follow(
37
      "/api/v1/events?follow=1",
38
      (data) => {
39
        try {
40
          const e = JSON.parse(data) as StreamEvent;
41
          this.events.fire(e);
42
        } catch {
43
          return;
44
        }
45
        this.soon();
46
      },
47
      () => this.soon(),
48
    );
49
    void this.refresh();
50
  }
51
52
  stop(): void {
53
    this.stream?.dispose();
54
    this.stream = undefined;
55
  }
56
57
  /** Refresh shortly, once for a burst of events. */
58
  soon(): void {
59
    if (this.timer) clearTimeout(this.timer);
60
    this.timer = setTimeout(() => void this.refresh(), 300);
61
  }
62
63
  async refresh(): Promise<void> {
64
    try {
65
      const [brief, agents, channels, projects, inbox, briefs] = await Promise.all([
66
        this.bridge.request<Brief>("GET", "/api/v1/brief"),
67
        this.bridge.request<Agent[]>("GET", "/api/v1/agents"),
68
        this.bridge.request<Channel[]>("GET", "/api/v1/channels"),
69
        this.bridge.request<Project[]>("GET", "/api/v1/projects"),
70
        this.bridge.request<InboxItem[]>("GET", "/api/v1/inbox"),
71
        this.bridge.request<Briefs>("GET", "/api/v1/briefs"),
72
      ]);
73
      Object.assign(this, { brief, agents, channels, projects, inbox, briefs, error: undefined });
74
    } catch (e) {
75
      this.error = (e as Error).message;
76
    }
77
    this.changed.fire();
78
  }
79
80
  /** The project of the first workspace folder, if it is one. */
81
  folderProject(): string | undefined {
82
    const f = vscode.workspace.workspaceFolders?.[0];
83
    return f ? projectForFolder(this.projects, f.uri.fsPath) : undefined;
84
  }
85
86
  dispose(): void {
87
    this.stop();
88
    this.changed.dispose();
89
    this.events.dispose();
90
  }
91
}