import * as plugins from './plugins.js'; import type * as interfaces from '../ts_interfaces/index.js'; import type { ControllerSocketClient } from './classes.controllersocketclient.js'; @plugins.deesElement.customElement('harness-codex-connections') export class HarnessCodexConnections extends plugins.deesElement.DeesElement { @plugins.deesElement.property({ attribute: false }) accessor client: ControllerSocketClient | undefined; @plugins.deesElement.property({ attribute: false }) accessor status: interfaces.IControllerStatus['harnesses'][number] | undefined; @plugins.deesElement.property({ attribute: false }) accessor project: interfaces.IControllerProject | undefined; @plugins.deesElement.state() private accessor profiles: interfaces.IControllerCodexProfile[] = []; @plugins.deesElement.state() private accessor selectedProfileId = ''; @plugins.deesElement.state() private accessor mapping: interfaces.IControllerCodexProjectMapping | undefined; @plugins.deesElement.state() private accessor mappingProfileId = ''; @plugins.deesElement.state() private accessor remoteDirectory = ''; @plugins.deesElement.state() private accessor editingName = ''; @plugins.deesElement.state() private accessor notice = ''; @plugins.deesElement.state() private accessor existingThreads: interfaces.IControllerSession[] = []; @plugins.deesElement.state() private accessor existingThreadsCursor: string | undefined; @plugins.deesElement.state() private accessor account: interfaces.IControllerCodexAccount | undefined; @plugins.deesElement.state() private accessor busy = false; @plugins.deesElement.state() private accessor error = ''; public static styles = [plugins.deesElement.css` :host { display: block; border-top: 1px solid var(--dees-color-border); padding-top: 12px; } .section { display: grid; gap: 8px; font-size: 13px; } .actions { display: flex; gap: 8px; flex-wrap: wrap; } .notice { color: var(--dees-color-text-warning); } .window { display: grid; gap: 4px; } progress { width: 100%; accent-color: var(--dees-color-accent-primary); } label { display: grid; gap: 4px; } input, select { box-sizing: border-box; width: 100%; padding: 8px; border: 1px solid var(--dees-color-border); border-radius: 6px; background: var(--dees-color-bg-canvas); color: inherit; font: inherit; } form, .mapping { display: grid; gap: 8px; border-top: 1px solid var(--dees-color-border); padding-top: 12px; } `]; public firstUpdated(): void { void this.refreshProfiles(); } public updated(changesArg: Map): void { super.updated(changesArg); if (changesArg.has('project') && changesArg.get('project') !== undefined) void this.refreshProfiles(); } private selectedProfile(): interfaces.IControllerCodexProfile | undefined { return this.profiles.find(profile => profile.id === this.selectedProfileId); } private connectionStatus(): interfaces.IControllerStatus['harnesses'][number] | undefined { return this.selectedProfile()?.status; } private async refreshProfiles(): Promise { if (!this.client) return; const projectId = this.project?.id; try { const response = await this.client.fire('controller.codex.profiles.list', projectId ? { projectId } : {}, { maxRetries: 0 }); if (this.project?.id !== projectId) return; if (this.mapping?.profileId !== response.mapping?.profileId || this.mapping?.remoteDirectory !== response.mapping?.remoteDirectory || this.mapping?.projectId !== response.mapping?.projectId) { this.existingThreads = []; this.existingThreadsCursor = undefined; } this.profiles = response.profiles; this.mapping = response.mapping; this.mappingProfileId = response.mapping?.profileId ?? response.profiles.find(profile => profile.mode === 'local')?.id ?? ''; this.remoteDirectory = response.mapping?.remoteDirectory ?? this.project?.directory ?? ''; this.selectProfile(this.profiles.some(profile => profile.id === this.selectedProfileId) ? this.selectedProfileId : this.mappingProfileId); } catch { this.error = 'Codex connections could not be loaded. Refresh to try again.'; } } private selectProfile(idArg: string): void { if (this.selectedProfileId !== idArg) this.account = undefined; this.selectedProfileId = idArg; this.editingName = this.selectedProfile()?.name ?? ''; } private changed(): void { this.dispatchEvent(new CustomEvent('codex-connection-changed', { bubbles: true, composed: true })); } private async act(operationArg: () => Promise): Promise { if (!this.client || this.busy) return; this.busy = true; this.error = ''; this.notice = ''; try { await operationArg(); await this.refreshProfiles(); this.changed(); } catch (error) { this.error = error instanceof Error ? error.message : 'Codex connection operation failed.'; } finally { this.busy = false; } } private async createProfile(eventArg: SubmitEvent): Promise { eventArg.preventDefault(); const form = eventArg.currentTarget as HTMLFormElement; const data = new FormData(form); const token = String(data.get('token') ?? ''); const input = { name: String(data.get('name') ?? '').trim(), serverUrl: String(data.get('serverUrl') ?? '').trim(), ...(token ? { token } : {}) }; (form.elements.namedItem('token') as HTMLInputElement).value = ''; await this.act(async () => { const response = await this.client!.fire('controller.codex.profile.create', input, { maxRetries: 0 }); this.selectedProfileId = response.profile.id; form.reset(); this.notice = 'Connection saved. Test it before mapping a project.'; }); } private async updateProfile(inputArg: { name?: string; token?: string | null; retire?: true }): Promise { const profile = this.selectedProfile(); if (!profile) return; await this.act(async () => { await this.client!.fire('controller.codex.profile.update', { profileId: profile.id, expectedCredentialRevision: profile.credentialRevision, ...inputArg, }, { timeoutMs: 90_000, maxRetries: 0 }); this.account = undefined; this.notice = 'Connection updated.'; }); } private async testProfile(): Promise { await this.act(async () => { await this.client!.fire('controller.codex.profile.test', { profileId: this.selectedProfileId }, { timeoutMs: 90_000, maxRetries: 0 }); this.notice = 'Connection is healthy.'; }); } private async saveMapping(): Promise { const project = this.project; if (!project) return; const profile = this.profiles.find(entry => entry.id === this.mappingProfileId); await this.act(async () => { await this.client!.fire('controller.codex.mapping.update', { projectId: project.id, profileId: this.mappingProfileId, remoteDirectory: profile?.mode === 'local' ? project.directory : this.remoteDirectory, }, { timeoutMs: 90_000, maxRetries: 0 }); this.notice = 'New conversations will use this connection and directory. Existing conversations keep their original connection.'; }); } private async listExistingThreads(nextArg = false): Promise { const project = this.project; if (!project || !this.client || this.busy) return; this.busy = true; this.error = ''; try { const response = await this.client.fire('controller.codex.threads.list', { projectId: project.id, ...(nextArg && this.existingThreadsCursor ? { cursor: this.existingThreadsCursor } : {}), }, { maxRetries: 0 }); if (this.project?.id !== project.id) return; this.existingThreads = response.sessions; this.existingThreadsCursor = response.nextCursor; this.notice = response.sessions.length ? '' : response.nextCursor ? 'No additional conversations on this page. Continue to the next page.' : 'No additional conversations found for this project directory.'; } catch { this.error = 'Existing Codex conversations could not be read. Check the project connection.'; } finally { this.busy = false; } } private async enrollThread(sessionArg: interfaces.IControllerSession): Promise { const project = this.project; if (!project) return; await this.act(async () => { await this.client!.fire('controller.codex.thread.enroll', { projectId: project.id, sessionId: sessionArg.id, }, { maxRetries: 0 }); this.existingThreads = this.existingThreads.filter(session => session.id.nativeId !== sessionArg.id.nativeId); this.notice = 'Conversation added to this project’s sidebar. Open it there to continue the same thread.'; }); } private async refreshAccount(): Promise { if (!this.client || this.busy) return; this.busy = true; this.error = ''; try { this.account = (await this.client.fire('controller.codex.account.get', { profileId: this.selectedProfileId }, { maxRetries: 0 })).account; } catch { this.error = 'Codex account details could not be read. Check the connection and try Refresh.'; } finally { this.busy = false; } } private async restart(): Promise { if (!this.client || this.busy) return; this.busy = true; this.error = ''; try { await this.client.fire('controller.codex.restart', { profileId: this.selectedProfileId }, { timeoutMs: 90_000, maxRetries: 0 }); this.account = undefined; await this.refreshProfiles(); this.changed(); } catch (error) { this.error = error instanceof Error ? error.message : 'Codex restart failed.'; } finally { this.busy = false; } if (!this.error) await this.refreshAccount(); } public render(): plugins.deesElement.TemplateResult { const account = this.account; const profile = this.selectedProfile(); const status = this.connectionStatus(); return plugins.deesElement.html`
Codex
${status?.connectionMode === 'remote' ? 'Remote connection' : status?.connectionMode === 'shared' ? 'Shared local app-server' : 'Local app-server'} · ${status?.state ?? 'not connected'}${status?.version ? ` · ${status.version}` : ''}
${profile?.serverUrl ? plugins.deesElement.html`
${profile.serverUrl} · ${profile.hasCredential ? 'Credential saved' : 'No credential'}
` : ''} ${status?.diagnostic ? plugins.deesElement.html`
${status.diagnostic.message}
` : ''} ${account ? plugins.deesElement.html`
${account.type === 'chatgpt' ? account.email ?? 'ChatGPT account' : account.type === 'apiKey' ? 'API key' : account.type === 'amazonBedrock' ? 'Amazon Bedrock' : 'Signed out'}${account.plan ? ` · ${account.plan}` : ''}
${account.rateLimits.flatMap(limit => limit.windows.map(window => plugins.deesElement.html`
${limit.name}${window.durationMinutes ? ` · ${window.durationMinutes >= 1440 ? `${Math.round(window.durationMinutes / 1440)} days` : `${window.durationMinutes / 60} hours`}` : ''}: ${Math.round(window.usedPercent)}% used ${window.resetsAt ? plugins.deesElement.html`Resets ${new Date(window.resetsAt).toLocaleString()}` : ''}
`))} ${account.lifetimeTokens !== undefined ? plugins.deesElement.html`
Account lifetime: ${account.lifetimeTokens.toLocaleString()} tokens
` : ''} ${!account.rateLimitsAvailable ? plugins.deesElement.html`
Rate limits are unavailable for this account.
` : ''} ${!account.usageAvailable ? plugins.deesElement.html`
Account token totals are unavailable.
` : ''} ` : ''} ${this.error ? plugins.deesElement.html`` : ''} ${this.notice ? plugins.deesElement.html`
${this.notice}
` : ''}
void this.refreshProfiles()}> void this.testProfile()}> void this.refreshAccount()}> void this.restart()}>
${status?.connectionMode === 'local' ? 'Restart stops work in AGL’s local Codex process.' : 'Reconnect closes only AGL’s connection. Work in the shared server continues.'} Queued prompts remain in AGL. If another client is active, use Resume queue after its turn finishes.
${profile?.state === 'active' ? plugins.deesElement.html`
void this.updateProfile({ name: this.editingName.trim() })}> ${profile.mode === 'remote' ? plugins.deesElement.html` void this.updateProfile({ retire: true })}>` : ''}
${profile.mode === 'remote' ? plugins.deesElement.html`
{ event.preventDefault(); const field = (event.currentTarget as HTMLFormElement).elements.namedItem('replacement') as HTMLInputElement; const token = field.value; field.value = ''; void this.updateProfile({ token }); }}>
void this.updateProfile({ token: null })}>
` : ''} ` : ''} ${this.project ? plugins.deesElement.html`
New Codex conversations in ${this.project.name} void this.saveMapping()}>
void this.listExistingThreads()}> ${this.existingThreadsCursor ? plugins.deesElement.html` void this.listExistingThreads(true)}>` : ''}
${this.existingThreads.map(session => plugins.deesElement.html`
${session.title}${session.status === 'busy' ? ' · active in another client' : ''} void this.enrollThread(session)}>
`)}
` : ''}
void this.createProfile(event)}>Add remote Codex connection
Credentials are encrypted with this controller host’s TPM.
`; } }