Reuse status dashboard message and simplify settings actions
This commit is contained in:
@@ -9,11 +9,7 @@ import {
|
|||||||
updateMoaSettings,
|
updateMoaSettings,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
export function MoaSettingsPanel({
|
export function MoaSettingsPanel() {
|
||||||
onRestartStack,
|
|
||||||
}: {
|
|
||||||
onRestartStack: () => void;
|
|
||||||
}) {
|
|
||||||
const [config, setConfig] = useState<MoaSettingsSnapshot | null>(null);
|
const [config, setConfig] = useState<MoaSettingsSnapshot | null>(null);
|
||||||
const [draft, setDraft] = useState<MoaSettingsSnapshot | null>(null);
|
const [draft, setDraft] = useState<MoaSettingsSnapshot | null>(null);
|
||||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||||
@@ -135,7 +131,6 @@ export function MoaSettingsPanel({
|
|||||||
setApiKeys((prev) => ({ ...prev, [name]: value }))
|
setApiKeys((prev) => ({ ...prev, [name]: value }))
|
||||||
}
|
}
|
||||||
onModelChange={setModel}
|
onModelChange={setModel}
|
||||||
onRestartStack={onRestartStack}
|
|
||||||
onSave={save}
|
onSave={save}
|
||||||
onTest={testModel}
|
onTest={testModel}
|
||||||
onToggle={() =>
|
onToggle={() =>
|
||||||
@@ -158,7 +153,6 @@ function MoaSettingsContent({
|
|||||||
draft,
|
draft,
|
||||||
onApiKeyChange,
|
onApiKeyChange,
|
||||||
onModelChange,
|
onModelChange,
|
||||||
onRestartStack,
|
|
||||||
onSave,
|
onSave,
|
||||||
onTest,
|
onTest,
|
||||||
onToggle,
|
onToggle,
|
||||||
@@ -174,7 +168,6 @@ function MoaSettingsContent({
|
|||||||
name: string,
|
name: string,
|
||||||
patch: Partial<MoaModelSettingsSnapshot>,
|
patch: Partial<MoaModelSettingsSnapshot>,
|
||||||
) => void;
|
) => void;
|
||||||
onRestartStack: () => void;
|
|
||||||
onSave: () => Promise<void>;
|
onSave: () => Promise<void>;
|
||||||
onTest: (name: string) => Promise<void>;
|
onTest: (name: string) => Promise<void>;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
@@ -206,7 +199,6 @@ function MoaSettingsContent({
|
|||||||
busy={busy}
|
busy={busy}
|
||||||
checking={checking}
|
checking={checking}
|
||||||
dirty={dirty}
|
dirty={dirty}
|
||||||
onRestartStack={onRestartStack}
|
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
savedAt={savedAt}
|
savedAt={savedAt}
|
||||||
/>
|
/>
|
||||||
@@ -256,14 +248,12 @@ function MoaSettingsActions({
|
|||||||
busy,
|
busy,
|
||||||
checking,
|
checking,
|
||||||
dirty,
|
dirty,
|
||||||
onRestartStack,
|
|
||||||
onSave,
|
onSave,
|
||||||
savedAt,
|
savedAt,
|
||||||
}: {
|
}: {
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
checking: string | null;
|
checking: string | null;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
onRestartStack: () => void;
|
|
||||||
onSave: () => Promise<void>;
|
onSave: () => Promise<void>;
|
||||||
savedAt: number | null;
|
savedAt: number | null;
|
||||||
}) {
|
}) {
|
||||||
@@ -275,29 +265,13 @@ function MoaSettingsActions({
|
|||||||
onClick={() => void onSave()}
|
onClick={() => void onSave()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{busy ? '저장 중…' : '저장'}
|
{busy ? '저장 중…' : 'MoA 저장'}
|
||||||
</button>
|
</button>
|
||||||
{savedAt && !dirty ? (
|
{savedAt && !dirty ? (
|
||||||
<small className="settings-hint">
|
<small className="settings-hint">
|
||||||
저장됨. 적용하려면 스택 재시작 필요.
|
저장됨. 적용하려면 스택 재시작 필요.
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
|
||||||
className="settings-restart"
|
|
||||||
disabled={busy || checking !== null}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
onRestartStack();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
스택 재시작
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ describe('SettingsPanel', () => {
|
|||||||
expect(html).toContain('MoA 참조 모델');
|
expect(html).toContain('MoA 참조 모델');
|
||||||
expect(html).toContain('패스트 모드');
|
expect(html).toContain('패스트 모드');
|
||||||
expect(html).toContain('Codex 실험 기능');
|
expect(html).toContain('Codex 실험 기능');
|
||||||
|
expect(html).toContain('변경 적용');
|
||||||
expect(html).toContain('불러오는 중');
|
expect(html).toContain('불러오는 중');
|
||||||
expect(html).toContain('Claude');
|
expect(html).toContain('Claude');
|
||||||
expect(html).toContain('계정');
|
expect(html).toContain('계정');
|
||||||
expect(html).toContain('전체 갱신');
|
expect(html).toContain('전체 갱신');
|
||||||
expect(html).toContain('스택 재시작');
|
expect(html).toContain('스택 재시작');
|
||||||
|
expect(html.match(/class="settings-restart"/g)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,20 +79,51 @@ export function SettingsPanel({
|
|||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ModelSettings onRestartStack={onRestartStack} />
|
<SettingsApplyBar onRestartStack={onRestartStack} />
|
||||||
|
|
||||||
<MoaSettingsPanel onRestartStack={onRestartStack} />
|
<ModelSettings />
|
||||||
|
|
||||||
|
<MoaSettingsPanel />
|
||||||
|
|
||||||
<FastModeSettings />
|
<FastModeSettings />
|
||||||
|
|
||||||
<CodexFeatureSettings onRestartStack={onRestartStack} />
|
<CodexFeatureSettings />
|
||||||
|
|
||||||
<AccountSettings onRestartStack={onRestartStack} />
|
<AccountSettings />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) {
|
function SettingsApplyBar({ onRestartStack }: { onRestartStack: () => void }) {
|
||||||
|
return (
|
||||||
|
<section className="settings-apply-bar" aria-label="변경 적용">
|
||||||
|
<div>
|
||||||
|
<strong>변경 적용</strong>
|
||||||
|
<small>
|
||||||
|
모델, MoA, Codex 실험 기능, 계정 변경은 저장 후 스택 재시작으로
|
||||||
|
적용됩니다.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="settings-restart"
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
onRestartStack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
스택 재시작
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModelSettings() {
|
||||||
const [config, setConfig] = useState<ModelConfigSnapshot | null>(null);
|
const [config, setConfig] = useState<ModelConfigSnapshot | null>(null);
|
||||||
const [draft, setDraft] = useState<ModelConfigSnapshot | null>(null);
|
const [draft, setDraft] = useState<ModelConfigSnapshot | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -193,29 +224,13 @@ function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) {
|
|||||||
onClick={() => void save()}
|
onClick={() => void save()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{busy ? '저장 중…' : '저장'}
|
{busy ? '저장 중…' : '모델 저장'}
|
||||||
</button>
|
</button>
|
||||||
{savedAt && !dirty ? (
|
{savedAt && !dirty ? (
|
||||||
<small className="settings-hint">
|
<small className="settings-hint">
|
||||||
저장됨. 적용하려면 스택 재시작 필요.
|
저장됨. 적용하려면 스택 재시작 필요.
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
|
||||||
className="settings-restart"
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
onRestartStack();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
스택 재시작
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -307,11 +322,7 @@ function FastModeSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodexFeatureSettings({
|
function CodexFeatureSettings() {
|
||||||
onRestartStack,
|
|
||||||
}: {
|
|
||||||
onRestartStack: () => void;
|
|
||||||
}) {
|
|
||||||
const [state, setState] = useState<CodexFeatureSnapshot | null>(null);
|
const [state, setState] = useState<CodexFeatureSnapshot | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -376,29 +387,11 @@ function CodexFeatureSettings({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="settings-actions">
|
{savedAt ? (
|
||||||
{savedAt ? (
|
<small className="settings-hint">
|
||||||
<small className="settings-hint">
|
저장됨. 적용하려면 상단의 스택 재시작을 눌러 주세요.
|
||||||
저장됨. 적용하려면 스택 재시작 필요.
|
</small>
|
||||||
</small>
|
) : null}
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
className="settings-restart"
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
onRestartStack();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
스택 재시작
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -436,7 +429,7 @@ function formatExpiry(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountSettings({ onRestartStack }: { onRestartStack: () => void }) {
|
function AccountSettings() {
|
||||||
const [data, setData] = useState<AccountData | null>(null);
|
const [data, setData] = useState<AccountData | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [perRowBusy, setPerRowBusy] = useState<string | null>(null);
|
const [perRowBusy, setPerRowBusy] = useState<string | null>(null);
|
||||||
@@ -561,24 +554,6 @@ function AccountSettings({ onRestartStack }: { onRestartStack: () => void }) {
|
|||||||
onSwitch={(index) => void handleSwitchCodex(index)}
|
onSwitch={(index) => void handleSwitchCodex(index)}
|
||||||
perRowBusy={perRowBusy}
|
perRowBusy={perRowBusy}
|
||||||
/>
|
/>
|
||||||
<div className="settings-actions">
|
|
||||||
<button
|
|
||||||
className="settings-restart"
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
onRestartStack();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
스택 재시작 (변경 적용)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -696,6 +696,39 @@ button:disabled {
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-apply-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 72px;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(214, 130, 88, 0.32);
|
||||||
|
border-radius: 10px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(214, 130, 88, 0.14), transparent 55%),
|
||||||
|
rgba(19, 24, 22, 0.94);
|
||||||
|
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-apply-bar > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-apply-bar strong {
|
||||||
|
color: var(--bg-ink);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-apply-bar small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-row-inline {
|
.settings-row-inline {
|
||||||
grid-template-columns: 80px minmax(0, 1fr) 100px;
|
grid-template-columns: 80px minmax(0, 1fr) 100px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
30
src/status-dashboard.test.ts
Normal file
30
src/status-dashboard.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { CACHE_DIR } from './config.js';
|
||||||
|
import {
|
||||||
|
readDashboardStatusMessageId,
|
||||||
|
writeDashboardStatusMessageId,
|
||||||
|
} from './status-dashboard.js';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
const messagesDir = path.join(CACHE_DIR, 'status-dashboard', 'messages');
|
||||||
|
fs.rmSync(path.join(messagesDir, 'test-status-channel.json'), {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dashboard status message id state', () => {
|
||||||
|
it('persists the tracked Discord status message id across restarts', async () => {
|
||||||
|
expect(readDashboardStatusMessageId('test-status-channel')).toBeNull();
|
||||||
|
|
||||||
|
writeDashboardStatusMessageId('test-status-channel', '1499810000000000000');
|
||||||
|
|
||||||
|
expect(readDashboardStatusMessageId('test-status-channel')).toBe(
|
||||||
|
'1499810000000000000',
|
||||||
|
);
|
||||||
|
expect(readDashboardStatusMessageId('other-channel')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { CACHE_DIR } from './config.js';
|
import { CACHE_DIR } from './config.js';
|
||||||
import { writeJsonFile } from './utils.js';
|
import { readJsonFile, writeJsonFile } from './utils.js';
|
||||||
import type { GroupStatus } from './group-queue.js';
|
import type { GroupStatus } from './group-queue.js';
|
||||||
import type { AgentType } from './types.js';
|
import type { AgentType } from './types.js';
|
||||||
|
|
||||||
@@ -37,6 +37,61 @@ export interface StatusSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_SNAPSHOT_DIR = path.join(CACHE_DIR, 'status-dashboard');
|
const STATUS_SNAPSHOT_DIR = path.join(CACHE_DIR, 'status-dashboard');
|
||||||
|
const STATUS_MESSAGE_STATE_DIR = path.join(STATUS_SNAPSHOT_DIR, 'messages');
|
||||||
|
|
||||||
|
interface StatusMessageState {
|
||||||
|
messageId: string;
|
||||||
|
statusChannelId: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeStatusChannelId(statusChannelId: string): string {
|
||||||
|
return statusChannelId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusMessageStatePath(statusChannelId: string): string {
|
||||||
|
return path.join(
|
||||||
|
STATUS_MESSAGE_STATE_DIR,
|
||||||
|
`${sanitizeStatusChannelId(statusChannelId)}.json`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDashboardStatusMessageId(
|
||||||
|
statusChannelId: string,
|
||||||
|
): string | null {
|
||||||
|
if (!statusChannelId) return null;
|
||||||
|
const parsed = readJsonFile<StatusMessageState>(
|
||||||
|
getStatusMessageStatePath(statusChannelId),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
parsed?.statusChannelId !== statusChannelId ||
|
||||||
|
typeof parsed.messageId !== 'string' ||
|
||||||
|
parsed.messageId.trim() === ''
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed.messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeDashboardStatusMessageId(
|
||||||
|
statusChannelId: string,
|
||||||
|
messageId: string,
|
||||||
|
): void {
|
||||||
|
if (!statusChannelId || !messageId) return;
|
||||||
|
fs.mkdirSync(STATUS_MESSAGE_STATE_DIR, { recursive: true });
|
||||||
|
const targetPath = getStatusMessageStatePath(statusChannelId);
|
||||||
|
const tempPath = `${targetPath}.tmp`;
|
||||||
|
writeJsonFile(
|
||||||
|
tempPath,
|
||||||
|
{
|
||||||
|
messageId,
|
||||||
|
statusChannelId,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
} satisfies StatusMessageState,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
fs.renameSync(tempPath, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
export function writeStatusSnapshot(snapshot: StatusSnapshot): void {
|
export function writeStatusSnapshot(snapshot: StatusSnapshot): void {
|
||||||
fs.mkdirSync(STATUS_SNAPSHOT_DIR, { recursive: true });
|
fs.mkdirSync(STATUS_SNAPSHOT_DIR, { recursive: true });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
buildWebUsageRowsForSnapshot,
|
buildWebUsageRowsForSnapshot,
|
||||||
formatStatusHeader,
|
formatStatusHeader,
|
||||||
renderUsageTable,
|
renderUsageTable,
|
||||||
|
shouldPurgeDashboardChannelOnStart,
|
||||||
summarizeWatcherTasks,
|
summarizeWatcherTasks,
|
||||||
} from './unified-dashboard.js';
|
} from './unified-dashboard.js';
|
||||||
|
|
||||||
@@ -61,6 +62,32 @@ describe('formatStatusHeader', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('shouldPurgeDashboardChannelOnStart', () => {
|
||||||
|
it('keeps the existing status message when a stored message id exists', () => {
|
||||||
|
expect(
|
||||||
|
shouldPurgeDashboardChannelOnStart({
|
||||||
|
purgeOnStart: true,
|
||||||
|
storedMessageId: 'status-message-1',
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('purges only when explicitly requested and no stored message id exists', () => {
|
||||||
|
expect(
|
||||||
|
shouldPurgeDashboardChannelOnStart({
|
||||||
|
purgeOnStart: true,
|
||||||
|
storedMessageId: null,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldPurgeDashboardChannelOnStart({
|
||||||
|
purgeOnStart: false,
|
||||||
|
storedMessageId: null,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('renderUsageTable', () => {
|
describe('renderUsageTable', () => {
|
||||||
const claudeRow: UsageRow = {
|
const claudeRow: UsageRow = {
|
||||||
name: 'Claude pro',
|
name: 'Claude pro',
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ import type { GroupQueue } from './group-queue.js';
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { isWatchCiTask } from './task-watch-status.js';
|
import { isWatchCiTask } from './task-watch-status.js';
|
||||||
import {
|
import {
|
||||||
|
readDashboardStatusMessageId,
|
||||||
readStatusSnapshots,
|
readStatusSnapshots,
|
||||||
|
writeDashboardStatusMessageId,
|
||||||
writeStatusSnapshot,
|
writeStatusSnapshot,
|
||||||
} from './status-dashboard.js';
|
} from './status-dashboard.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -169,6 +171,13 @@ export async function purgeDashboardChannel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldPurgeDashboardChannelOnStart(args: {
|
||||||
|
purgeOnStart?: boolean;
|
||||||
|
storedMessageId: string | null;
|
||||||
|
}): boolean {
|
||||||
|
return args.purgeOnStart === true && !args.storedMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshChannelMeta(
|
async function refreshChannelMeta(
|
||||||
opts: UnifiedDashboardOptions,
|
opts: UnifiedDashboardOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -708,8 +717,17 @@ export async function startUnifiedDashboard(
|
|||||||
|
|
||||||
const isRenderer = opts.serviceAgentType === 'claude-code';
|
const isRenderer = opts.serviceAgentType === 'claude-code';
|
||||||
const statusJid = `dc:${opts.statusChannelId}`;
|
const statusJid = `dc:${opts.statusChannelId}`;
|
||||||
|
if (isRenderer) {
|
||||||
|
statusMessageId = readDashboardStatusMessageId(opts.statusChannelId);
|
||||||
|
}
|
||||||
|
|
||||||
if (isRenderer && opts.purgeOnStart) {
|
if (
|
||||||
|
isRenderer &&
|
||||||
|
shouldPurgeDashboardChannelOnStart({
|
||||||
|
purgeOnStart: opts.purgeOnStart,
|
||||||
|
storedMessageId: statusMessageId,
|
||||||
|
})
|
||||||
|
) {
|
||||||
await purgeDashboardChannel(opts);
|
await purgeDashboardChannel(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,15 +764,28 @@ export async function startUnifiedDashboard(
|
|||||||
},
|
},
|
||||||
'Dashboard content empty, skipping render',
|
'Dashboard content empty, skipping render',
|
||||||
);
|
);
|
||||||
statusMessageId = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusMessageId && channel.editMessage) {
|
if (statusMessageId && channel.editMessage) {
|
||||||
await channel.editMessage(statusJid, statusMessageId, content);
|
try {
|
||||||
} else if (channel.sendAndTrack) {
|
await channel.editMessage(statusJid, statusMessageId, content);
|
||||||
|
writeDashboardStatusMessageId(opts.statusChannelId, statusMessageId);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ err, messageId: statusMessageId },
|
||||||
|
'Dashboard status message edit failed; sending a fresh tracked message',
|
||||||
|
);
|
||||||
|
statusMessageId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusMessageId && channel.sendAndTrack) {
|
||||||
const id = await channel.sendAndTrack(statusJid, content);
|
const id = await channel.sendAndTrack(statusJid, content);
|
||||||
if (id) statusMessageId = id;
|
if (id) {
|
||||||
|
statusMessageId = id;
|
||||||
|
writeDashboardStatusMessageId(opts.statusChannelId, id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!dashboardUpdateLogged) {
|
if (!dashboardUpdateLogged) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
Reference in New Issue
Block a user