Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"src/renderer/locales/settings-web-search-copy.ts",
"src/renderer/locales/shell-copy.ts",
"src/renderer/locales/shell-remaining-copy.ts",
"src/renderer/locales/storage-retention-copy.ts",
"src/renderer/locales/storage-usage-copy.ts",
"src/renderer/locales/task-readiness-copy.ts",
"src/renderer/locales/work-board-error-copy.ts",
Expand Down Expand Up @@ -1455,6 +1456,15 @@
"actionFactories": [],
"dependencyPaths": {}
},
"src/renderer/locales/storage-retention-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {}
},
"src/renderer/locales/storage-usage-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/runtime-host-renderer-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ function request(
return client.request(operation, HOST_OPERATION_SPECS[operation].decodeInput(value));
case 'scheduled-task.query':
return client.request(operation, HOST_OPERATION_SPECS[operation].decodeInput(value));
case 'storage.retention.query':
return client.request(operation, HOST_OPERATION_SPECS[operation].decodeInput(value));
case 'storage.retention.set':
return client.request(operation, HOST_OPERATION_SPECS[operation].decodeInput(value));
case 'storage.usage.query':
return client.request(operation, HOST_OPERATION_SPECS[operation].decodeInput(value));
case 'storage.usage.sessions.query':
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ import type {
SessionRemovePreviewResult,
SessionStorageUsage,
StorageUsageQueryResult,
StorageRetentionQueryResult,
StorageRetentionPolicy,
StorageRetentionSetInput,
} from '@maka/runtime-host/protocol';
import type { TestProxyInput } from '@maka/core/settings/network-settings';
import type { ExternalSessionImportIpcResult } from './external-session-import-result.js';
Expand Down Expand Up @@ -1853,6 +1856,8 @@ export interface MakaBridge {
test(input: { provider?: WebSearchProvider; apiKey?: string }, host?: DesktopRuntimeHostRef): Promise<WebSearchResponse>;
};
storage: {
retentionQuery(host?: DesktopRuntimeHostRef): Promise<StorageRetentionQueryResult>;
retentionSet(input: StorageRetentionSetInput, host?: DesktopRuntimeHostRef): Promise<StorageRetentionPolicy>;
/** One Runtime Host's State Root footprint. Read-only; nothing is reclaimed. */
usage(host?: DesktopRuntimeHostRef): Promise<StorageUsageQueryResult>;
/**
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ import {
type SessionRemovePreviewResult,
type SessionStorageUsage,
type StorageUsageQueryResult,
type StorageRetentionQueryResult,
type StorageRetentionPolicy,
type StorageRetentionSetInput,
} from '@maka/runtime-host/protocol';
import type { PlanControlIpcResult } from '../shared/plan-mode-ipc.js';
import { createSessionStorageUsageReader } from './session-storage-usage.js';
Expand Down Expand Up @@ -3735,6 +3738,12 @@ const makaBridge = {
},
},
storage: {
async retentionQuery(host?: DesktopRuntimeHostRef): Promise<StorageRetentionQueryResult> {
return scopedRuntimeHost(await selectedRuntimeHostScope(host)).query('storage.retention.query', {});
},
async retentionSet(input: StorageRetentionSetInput, host?: DesktopRuntimeHostRef): Promise<StorageRetentionPolicy> {
return scopedRuntimeHost(await selectedRuntimeHostScope(host)).command('storage.retention.set', input);
},
async usage(host?: DesktopRuntimeHostRef): Promise<StorageUsageQueryResult> {
return scopedRuntimeHost(await selectedRuntimeHostScope(host)).query('storage.usage.query', {});
},
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload/runtime-host-renderer-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ export const RENDERER_RUNTIME_HOST_QUERY_OPERATIONS = [
'execution.inspect.query',
'scheduled-task.query',
// Read-only State Root and per-task size measurement; it reclaims nothing.
'storage.retention.query',
'storage.usage.query',
'storage.usage.sessions.query',
] as const satisfies readonly (keyof OperationSpecMap)[];

export const RENDERER_RUNTIME_HOST_COMMAND_OPERATIONS = [
'storage.retention.set',
'daily-review.mutate',
'scheduled-task.mutate',
'web-search.execute',
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/features/storage-usage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ export { StorageUsageSection } from './ui/storage-usage-section.js';
export { TaskStorageSize } from './ui/task-storage-size.js';
export { StorageUsageServicesProvider } from './services-context.js';
export type { StorageUsageHostTarget, StorageUsageServices } from './ports.js';

export { StorageRetentionSection } from './ui/storage-retention-section.js';
4 changes: 3 additions & 1 deletion apps/desktop/src/renderer/features/storage-usage/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import type { SessionStorageUsage, StorageUsageQueryResult } from '@maka/runtime-host/protocol';
import type { SessionStorageUsage, StorageUsageQueryResult, StorageRetentionQueryResult, StorageRetentionSetInput, StorageRetentionPolicy } from '@maka/runtime-host/protocol';

/** The Runtime Host a Settings page is pointed at, stated structurally. */
export interface StorageUsageHostTarget {
Expand All @@ -30,6 +30,8 @@ export interface StorageUsageHostTarget {
* measurements. Nothing here reclaims or deletes data.
*/
export interface StorageUsageServices {
loadRetention?(host: StorageUsageHostTarget): Promise<StorageRetentionQueryResult>;
setRetention?(host: StorageUsageHostTarget, input: StorageRetentionSetInput): Promise<StorageRetentionPolicy>;
/** One Runtime Host's State Root footprint. */
loadUsage(host: StorageUsageHostTarget): Promise<StorageUsageQueryResult>;
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { useEffect, useRef, useState } from 'react';
import { Button, Selector, Switch, formatBytes, useUiLocale } from '@maka/ui';
import { RETENTION_DAYS, type StorageRetentionQueryResult, type RetentionDays } from '@maka/runtime-host/protocol';
import { SettingsRow, SettingsSection, useOptionalRuntimeHostSettingsGenerationKey, useOptionalRuntimeHostSettingsTarget } from '../../../application/contracts/settings-presentation/index.js';
import { getStorageRetentionCopy } from '../../../locales/storage-retention-copy.js';
import { useOptionalStorageUsageServices } from '../services-context.js';

export function StorageRetentionSection() {
const host = useOptionalRuntimeHostSettingsTarget();
const hostKey = useOptionalRuntimeHostSettingsGenerationKey();
const services = useOptionalStorageUsageServices();
const locale = useUiLocale(); const copy = getStorageRetentionCopy(locale);
const [snapshot, setSnapshot] = useState<{ key: string | undefined; result: StorageRetentionQueryResult }>();
const [failed, setFailed] = useState(false); const [saving, setSaving] = useState(false); const [attempt, setAttempt] = useState(0);
const generation = useRef(0);
useEffect(() => {
const current = ++generation.current; setFailed(false); setSaving(false);
if (!host || !services?.loadRetention) return;
services.loadRetention(host).then(result => {
if (generation.current === current) setSnapshot({ key: hostKey, result });
}, () => { if (generation.current === current) setFailed(true); });
return () => { generation.current++; };
}, [host, hostKey, services, attempt]);
if (!host || !services?.loadRetention || !services.setRetention) return null;
const result = snapshot && snapshot.key === hostKey ? snapshot.result : undefined;
const disabled = !result || saving || failed;
async function save(enabled: boolean, days: RetentionDays) {
if (!host || !result || !services?.setRetention) return;
const current = generation.current; setSaving(true);
try {
const policy = await services.setRetention(host, { enabled, days, expectedRevision: result.policy.revision });
if (generation.current === current) {
setSnapshot({ key: hostKey, result: { ...result, policy } });
setAttempt(value => value + 1);
}
} catch { if (generation.current === current) setFailed(true); }
finally { if (generation.current === current) setSaving(false); }
}
const date = (time: number) => new Date(time).toLocaleString(locale);
return <SettingsSection title={copy.title} description={copy.help} action={<Button size="sm" variant="ghost" isDisabled={saving} label={copy.refresh} onClick={() => setAttempt(value => value + 1)} />}>
<SettingsRow label={copy.enabled} end={<Switch label={copy.enabled} isLabelHidden value={result?.policy.enabled ?? false} isDisabled={disabled} onChange={enabled => void save(enabled, result?.policy.days ?? 30)} />} />
<SettingsRow label={copy.days} end={<Selector label={copy.days} isLabelHidden value={String(result?.policy.days ?? 30)} options={RETENTION_DAYS.map(days => ({ value: String(days), label: copy.dayOption(days) }))} isDisabled={disabled} onChange={days => void save(result?.policy.enabled ?? false, Number(days) as RetentionDays)} />} />
<SettingsRow label={failed ? copy.failed : !result ? copy.loading : result.preview.eligibleAt === null ? copy.disabled : copy.preview(result.preview.count, date(result.preview.eligibleAt))} />
{result?.lastDeletion && <SettingsRow label={copy.cleanup(result.lastDeletion.count, result.lastDeletion.estimatedBytes === null ? copy.unknownBytes : formatBytes(result.lastDeletion.estimatedBytes, locale), date(result.lastDeletion.at))} />}
{result?.lastSweep && <SettingsRow label={copy.needsReview(result.lastSweep.needsReview)} />}
</SettingsSection>;
}
64 changes: 64 additions & 0 deletions apps/desktop/src/renderer/locales/storage-retention-copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
interface StorageRetentionCopy {
title: string;
help: string;
enabled: string;
days: string;
refresh: string;
loading: string;
failed: string;
dayOption: (days: number) => string;
preview: (count: number, date: string) => string;
disabled: string;
cleanup: (count: number, size: string, date: string) => string;
unknownBytes: string;
needsReview: (count: number) => string;
}

const COPY = {
en: {
title: 'Automatic cleanup on this Host',
help: 'Applies to all archived tasks on this Host. The clock starts when enabled or the duration changes. Pinned tasks are kept.',
enabled: 'Delete archived tasks automatically', days: 'Delete after', refresh: 'Refresh',
loading: 'Loading retention settings…', failed: 'Could not load or save retention settings. Refresh before trying again.',
dayOption: (days: number) => `${days} days`,
preview: (count: number, date: string) => `${count} archived tasks become eligible on ${date}.`,
disabled: 'Automatic deletion is off.',
cleanup: (count: number, size: string, date: string) => `Last automatic cleanup: deleted ${count} tasks (${size}) on ${date}.`,
unknownBytes: 'size unavailable', needsReview: (count: number) => `${count} tasks need manual review.`,
},
'zh-CN': {
title: '此 Host 的自动清理', help: '适用于此 Host 的所有已归档任务。启用或修改天数时重新计时,已置顶任务会保留。',
enabled: '自动删除已归档任务', days: '保留时长', refresh: '刷新', loading: '正在读取自动清理设置…', failed: '无法读取或保存设置,请刷新后重试。',
dayOption: (days: number) => `${days} 天`, preview: (count: number, date: string) => `${count} 个已归档任务将在 ${date} 后符合清理条件。`,
disabled: '自动删除已关闭。', cleanup: (count: number, size: string, date: string) => `上次自动清理:${date} 删除了 ${count} 个任务(${size})。`,
unknownBytes: '大小未知', needsReview: (count: number) => `${count} 个任务需要手动检查。`,
},
'zh-TW': {
title: '此 Host 的自動清理', help: '適用於此 Host 的所有已封存任務。啟用或修改天數時重新計時,已置頂任務會保留。',
enabled: '自動刪除已封存任務', days: '保留時長', refresh: '重新整理', loading: '正在讀取自動清理設定…', failed: '無法讀取或儲存設定,請重新整理後重試。',
dayOption: (days: number) => `${days} 天`, preview: (count: number, date: string) => `${count} 個已封存任務將在 ${date} 後符合清理條件。`,
disabled: '自動刪除已關閉。', cleanup: (count: number, size: string, date: string) => `上次自動清理:${date} 刪除了 ${count} 個任務(${size})。`,
unknownBytes: '大小未知', needsReview: (count: number) => `${count} 個任務需要手動檢查。`,
},
} satisfies UiCatalog<StorageRetentionCopy>;
export function getStorageRetentionCopy(locale: UiLocale) { return COPY[locale]; }
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function createDesktopStorageUsageServices(
bridge: DesktopStorageUsageBridge = window.maka,
): StorageUsageServices {
return {
loadRetention: (host) => bridge.storage.retentionQuery(host),
setRetention: (host, input) => bridge.storage.retentionSet(input, host),
loadUsage: (host) => bridge.storage.usage(host),
// No host argument: each task is measured by the Host that holds it, and
// the bridge routes by the projected id for exactly that reason.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/settings/settings-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const SETTINGS_SECTION_SCOPES: Record<
'bot-chat': 'client',
search: 'runtime-host',
usage: 'runtime-host',
'archived-tasks': 'client',
'archived-tasks': 'mixed',
'import-tasks': 'runtime-host',
'daily-review': 'runtime-host',
data: 'mixed',
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/renderer/settings/tasks-settings-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type { DesktopSessionSummary } from '../../preload/bridge-contract.js';
import type { SessionCatalogController } from '../application/contracts/session-catalog/session-catalog-state.js';
import { getSettingsTasksCopy } from '../locales/settings-tasks-copy.js';
import { getStorageUsageCopy } from '../locales/storage-usage-copy.js';
import { TaskStorageSize } from '../features/storage-usage/index.js';
import { TaskStorageSize, StorageRetentionSection } from '../features/storage-usage/index.js';
import { SettingsPage, SettingsSection } from './settings-section';
import { isOrphanedSubagentTask } from './task-catalog-rows';

Expand Down Expand Up @@ -89,13 +89,15 @@ export function TasksSettingsPage(
if (props.scope.rows.length === 0) {
return (
<SettingsPage>
<StorageRetentionSection />
<EmptyState title={copy.emptyTitle} description={copy.emptyBody} />
</SettingsPage>
);
}

return (
<SettingsPage as="section" aria-label={copy.listAria}>
<StorageRetentionSection />
{props.scope.controls}
<SettingsSection description={getStorageUsageCopy(locale).taskSizeNote}>
{visible.length === 0 ? (
Expand Down
20 changes: 19 additions & 1 deletion apps/desktop/stories/settings/settings-pages.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import {
AppUpdateServicesProvider,
type AppUpdateServices,
} from '../../src/renderer/features/app-update/index.js';
import { StorageUsageServicesProvider, type StorageUsageServices } from '../../src/renderer/features/storage-usage/index.js';
import type { StorageRetentionPolicy } from '@maka/runtime-host/protocol';
import type { SessionSummary } from '@maka/core/session';
import { revisionFamilySessionIds } from '@maka/core/session-revisions';
import type {
Expand Down Expand Up @@ -1970,11 +1972,27 @@ function fieldChrome(element: HTMLElement) {
* reach a provider its own component renders.
*/
function SettingsStory(props: SettingsStoryProps) {
// IO fixture for the actual production feature, on the existing Settings frame.
const [storageServices] = useState<StorageUsageServices>(() => {
let policy: StorageRetentionPolicy = { enabled: false, days: 30, enabledAt: null, revision: 0 };
return {
loadUsage: async () => ({ measuredAt: Date.now(), totals: [], reclaimableBytes: 0, worktreeCount: 0 }),
loadSessionUsage: async () => ({}),
loadRetention: async () => ({ policy, preview: { count: policy.enabled ? archivedTaskSessions.length : 0, eligibleAt: policy.enabledAt === null ? null : policy.enabledAt + policy.days * 24 * 60 * 60 * 1000 }, lastSweep: null, lastDeletion: null }),
setRetention: async (_host, input) => {
if (input.expectedRevision !== policy.revision) throw new Error('Retention revision changed');
policy = { enabled: input.enabled, days: input.days, enabledAt: input.enabled ? Date.now() : null, revision: policy.revision + 1 };
return policy;
},
};
});
return (
<ToastProvider>
<AppUpdateServicesProvider services={settingsAppUpdateServices}>
<AppUpdateProvider>
<SettingsStoryFrame {...props} />
<StorageUsageServicesProvider services={storageServices}>
<SettingsStoryFrame {...props} />
</StorageUsageServicesProvider>
</AppUpdateProvider>
</AppUpdateServicesProvider>
</ToastProvider>
Expand Down
3 changes: 2 additions & 1 deletion docs/astryx-surface-file-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.6.3` (195 component exports).

Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding.

**Totals:** 322 files — blocker 0, reimplementation 0, polish 4, aligned 318.
**Totals:** 323 files — blocker 0, reimplementation 0, polish 4, aligned 319.

## Exclusions (explicit)

Expand Down Expand Up @@ -119,6 +119,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `apps/desktop/src/renderer/features/session-settings/services-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `apps/desktop/src/renderer/features/session-settings/ui/session-settings-provider.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `apps/desktop/src/renderer/features/storage-usage/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `apps/desktop/src/renderer/features/storage-usage/ui/storage-retention-section.tsx` | other | Button, Selector, Switch | aligned — uses Astryx (Button, Selector, Switch) | aligned |
| `apps/desktop/src/renderer/features/storage-usage/ui/storage-usage-section.tsx` | other | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned |
| `apps/desktop/src/renderer/features/storage-usage/ui/task-storage-size.tsx` | other | Text | aligned — uses Astryx (Text) | aligned |
| `apps/desktop/src/renderer/features/task-entry/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
Expand Down
Loading