File size: 6,607 Bytes
5710dd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
 * App shell — selection state and session resume. There is no live event
 * push anymore: the v2 socket (`/api/v2/ws`) that fed the core/session/agent
 * event streams was removed server-side, so Service panels and the pending
 * interactions card fetch on demand and the sidebar polls.
 * Layout: header / icon rail / view. The `chat` view is a strip of the
 * left sidebar (a workspace → session tree), the chat column, and the
 * right dock (`RightPanel`) merging the transcript audit, the agent
 * inspector, and the session pane under Audit / Agent / State / Session
 * tabs;
 * the `models` view is the full-width model catalog; the `services` view is
 * the full-width app-scope Service reflection (`AppServicesView`); the
 * `workspace` view is the workspace-scope counterpart
 * (`WorkspaceServicesView`, with a workspace picker on top); the
 * `bash` view is the full-width `IBashParserService` playground
 * (`BashParserView`); the `di` view is the engine's Service × Effect × DI
 * debug surface (`DiInspectionView`); the `search` view is the full-width
 * global message search (`SearchView`) whose hits navigate back into the
 * chat timeline.
 */

import { ISessionManager } from '@moonshot-ai/agent-core-v2/app/sessionManager/sessionManager';
import { useEffect, useState } from 'react';

import type { AuditTrail } from './audit/trail';
import { AppServicesView } from './components/AppServicesView';
import { BashParserView } from './components/BashParserView';
import { ChatView, type ChatJump } from './components/ChatView';
import { DiInspectionView } from './components/DiInspectionView';
import { FsSuggestView } from './components/FsSuggestView';
import { ModelCatalogView } from './components/ModelCatalogView';
import { NavRail, type AppView } from './components/NavRail';
import { RightPanel } from './components/RightPanel';
import { SearchView } from './components/SearchView';
import { ServerSwitcher } from './components/ServerSwitcher';
import { Sidebar } from './components/Sidebar';
import { WorkspaceServicesView } from './components/WorkspaceServicesView';
import { useConnection } from './connection';
import type { SearchHit } from './search/api';
import { errorMessage } from './ui';

export function App() {
  const { klient, baseUrl, disconnect } = useConnection();
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [agentId, setAgentId] = useState('main');
  const [view, setView] = useState<AppView>('chat');
  const [ready, setReady] = useState(false);
  const [resumeError, setResumeError] = useState<unknown>(null);
  /** Audit trail of the chat view's transcript channel, rendered in the right dock. */
  const [trail, setTrail] = useState<AuditTrail | null>(null);
  /** Pending chat navigation requested from another view (search result click). */
  const [jump, setJump] = useState<ChatJump | null>(null);

  // Resume (materialize) the session on the server when it is selected, so
  // session / agent scoped Services become reachable. Session lifecycle lives
  // on the workspace handler (Workspace scope): the index yields the
  // session's workspaceId, then the handler resumes it.
  useEffect(() => {
    if (sessionId === null) return;
    let cancelled = false;
    setReady(false);
    setResumeError(null);
    klient
      .core(ISessionManager)
      .resume(sessionId)
      .then((session) => {
        if (session === undefined) throw new Error(`session ${sessionId} does not exist`);
      })
      .then(() => {
        if (!cancelled) setReady(true);
      })
      .catch((error: unknown) => {
        if (!cancelled) setResumeError(error);
      });
    return () => {
      cancelled = true;
    };
  }, [klient, sessionId]);

  // Switching servers invalidates every session/agent selection: sessions
  // belong to the server they were listed from.
  useEffect(() => {
    setSessionId(null);
    setAgentId('main');
    setJump(null);
  }, [baseUrl]);

  // A search hit opens the chat view at its session / agent / turn / step.
  // Title hits belong to the session (agent '') and carry no turn: switch
  // over without a scroll target.
  const openSearchHit = (hit: SearchHit): void => {
    setSessionId(hit.sessionId);
    setAgentId(hit.agentId === '' ? 'main' : hit.agentId);
    setView('chat');
    setJump({
      turnId: hit.turn === undefined ? undefined : `t${hit.turn}`,
      stepId: hit.stepId,
      nonce: Date.now(),
    });
  };

  return (
    <div className="flex h-screen flex-col">
      <header className="flex items-center gap-3 border-b border-neutral-800 px-4 py-1.5">
        <span className="text-[12px] font-bold tracking-widest text-neutral-200">KIMI INSPECT</span>
        <ServerSwitcher />
        <div className="flex-1" />
        <button
          className="text-[11px] text-neutral-500 hover:text-neutral-300"
          onClick={disconnect}
        >
          Disconnect
        </button>
      </header>
      <div className="flex min-h-0 flex-1">
        <NavRail view={view} onChange={setView} />
        {view === 'services' ? (
          <AppServicesView />
        ) : view === 'workspace' ? (
          <WorkspaceServicesView />
        ) : view === 'suggest' ? (
          <FsSuggestView />
        ) : view === 'bash' ? (
          <BashParserView />
        ) : view === 'di' ? (
          <DiInspectionView />
        ) : view === 'models' ? (
          <ModelCatalogView
            onOpenSession={(id) => {
              setSessionId(id);
              setView('chat');
            }}
          />
        ) : view === 'search' ? (
          <SearchView onOpenResult={openSearchHit} />
        ) : (
          <>
            <Sidebar activeSessionId={sessionId} onSelectSession={setSessionId} />
            {resumeError !== null ? (
              <div className="flex flex-1 items-center justify-center p-6 text-center text-[12px] text-red-400">
                Failed to open session: {errorMessage(resumeError)}
              </div>
            ) : (
              <ChatView
                sessionId={sessionId}
                agentId={agentId}
                ready={ready}
                onTrailChange={setTrail}
                jump={jump}
                onJumpHandled={() => setJump(null)}
                onOpenSearchHit={openSearchHit}
              />
            )}
            <RightPanel
              sessionId={sessionId}
              agentId={agentId}
              onAgentChange={setAgentId}
              ready={ready}
              trail={trail}
            />
          </>
        )}
      </div>
    </div>
  );
}