feat: add app management, local inference controls, and task statistics

This commit is contained in:
mesalogo
2026-09-18 08:20:45 +08:00
parent 6dd6297414
commit bbae893c45
103 changed files with 7962 additions and 1490 deletions
+30 -4
View File
@@ -30,6 +30,22 @@ export type MagicNoteCommentMode = z.infer<typeof magicNoteCommentModeSchema>
export const updateSourceSchema = z.enum(['github', 'mirror'])
export type UpdateSource = z.infer<typeof updateSourceSchema>
export const builtInApplicationIds = ['magic-notes', 'knowledge', 'heartbeat', 'local-inference'] as const
export type BuiltInApplicationId = typeof builtInApplicationIds[number]
export const editableApplicationIds = ['magic-notes', 'local-inference'] as const
export type EditableApplicationId = typeof editableApplicationIds[number]
export const defaultApplicationNavigation = {
order: [...editableApplicationIds],
pinned: { 'magic-notes': true, 'local-inference': true }
}
export const applicationNavigationSchema = z.object({
order: z.array(z.enum(editableApplicationIds)).length(editableApplicationIds.length)
.refine(ids => new Set(ids).size === editableApplicationIds.length, 'Each application must occur exactly once'),
pinned: z.object({
'magic-notes': z.boolean(), 'local-inference': z.boolean()
}).strict()
}).strict()
const applicationPreferencesSchema = z
.object({
checkUpdatesOnStartup: z.boolean(),
@@ -38,10 +54,12 @@ const applicationPreferencesSchema = z
localToolEnvironment: localToolEnvironmentSettingsSchema,
conversationHtmlRenderingEnabled: z.boolean(),
remoteProjectsEnabled: z.boolean(),
magicNotesEnabled: z.boolean(),
magicNotesShowIncompleteTodoCount: z.boolean(),
magicNoteCommentMode: magicNoteCommentModeSchema,
magicNoteCommentFormat: magicNoteCommentFormatSchema
applicationNavigation: applicationNavigationSchema.default(defaultApplicationNavigation),
localInferenceEnabled: z.boolean().default(true),
magicNotesEnabled: z.boolean().default(true),
magicNotesShowIncompleteTodoCount: z.boolean().default(true),
magicNoteCommentMode: magicNoteCommentModeSchema.default('immediate'),
magicNoteCommentFormat: magicNoteCommentFormatSchema.default('combined')
})
.strict()
@@ -53,6 +71,14 @@ export const applicationSettingsSchema = applicationPreferencesSchema
export const applicationSettingsUpdateSchema = applicationPreferencesSchema
.partial()
.extend({
applicationNavigation: applicationNavigationSchema.optional(),
localInferenceEnabled: z.boolean().optional(),
magicNotesEnabled: z.boolean().optional(),
magicNotesShowIncompleteTodoCount: z.boolean().optional(),
magicNoteCommentMode: magicNoteCommentModeSchema.optional(),
magicNoteCommentFormat: magicNoteCommentFormatSchema.optional()
})
.refine((input) => Object.keys(input).length > 0, {
message: 'At least one application setting is required'
})
+10
View File
@@ -4,6 +4,7 @@ import {
builtInDefaultProjectSeedDescription,
builtInDefaultProjectSeedName,
activityHistorySnapshotSchema,
executionStatsInputSchema,
conversationBranchInputSchema,
conversationMessageSchema,
conversationSubagentActivitySchema,
@@ -37,6 +38,15 @@ const untouchedProject: AssistantProject = {
updatedAt: '2026-08-01T00:00:00.000Z'
}
it('requires exactly one execution statistics scope and rejects renderer timing inputs', () => {
const id = untouchedProject.id
expect(executionStatsInputSchema.parse({ conversationId: id })).toEqual({ conversationId: id })
expect(executionStatsInputSchema.parse({ projectId: id })).toEqual({ projectId: id })
for (const input of [{}, { conversationId: id, projectId: id }, { projectId: 'bad' }, { projectId: id, asOf: 100 }]) {
expect(executionStatsInputSchema.safeParse(input).success).toBe(false)
}
})
describe('agent question contracts', () => {
it('returns recovered questions only in live snapshots, not saved headers', () => {
const id = '00000000-0000-4000-8000-000000000901'
+25 -2
View File
@@ -4,6 +4,24 @@ import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
import { sshHostIdSchema } from './ssh-host-contracts'
export const assistantIdSchema = z.string().uuid()
export const executionStatsInputSchema = z.union([
z.object({ conversationId: assistantIdSchema }).strict(),
z.object({ projectId: assistantIdSchema }).strict()
])
export type ExecutionStatsInput = z.infer<typeof executionStatsInputSchema>
export interface ExecutionStats {
/** Sum of evidenced top-level reply intervals, including tools and approvals. */
durationMs: number
requestCount: number
/** Requests/replies with missing timing evidence; confirmed live intervals are complete as of asOf. */
incompleteRequestCount: number
/** Requests currently leased by Main; does not authorize renderer extrapolation. */
activeRequestCount: number
/** Epoch milliseconds at query time. */
asOf: number
/** Project card totals; empty for conversation queries. */
taskDurations: Array<{ id: string; durationMs: number; incompleteRequestCount: number }>
}
export const interactiveWorkModes = ['ask', 'execute'] as const
export const workModeSchema = z.enum(interactiveWorkModes)
export const legacyWorkModeSchema = z.enum([
@@ -809,13 +827,18 @@ export const scheduleCreateSchema = z
prompt: z.string().trim().min(1).max(100_000),
workMode: workModeSchema.default('execute'),
recurrence: z.enum(['once', 'daily', 'weekly']),
nextRunAt: z.string().datetime({ offset: true })
nextRunAt: z.string().datetime({ offset: true }),
runImmediately: z.boolean().optional()
})
.strict()
.refine((input) => !input.runImmediately || input.recurrence === 'once', {
message: '立即执行只支持单次任务',
path: ['recurrence']
})
export type ScheduleCreateInput = z.input<typeof scheduleCreateSchema>
export type AssistantSchedule = z.output<typeof scheduleCreateSchema> & {
export type AssistantSchedule = Omit<z.output<typeof scheduleCreateSchema>, 'runImmediately'> & {
id: string
taskId: string
conversationId: string
+5
View File
@@ -39,6 +39,8 @@ import {
type AssistantHeartbeatRun,
type AssistantExpert,
type AssistantTask,
type ExecutionStats,
type ExecutionStatsInput,
type ActivityHistorySnapshot,
type ActivityRecord,
type TokenUsageSummary,
@@ -1744,6 +1746,7 @@ export type KnowledgeSearchReference = {
}
export type DesktopApi = {
localInference: import('./local-inference-contracts').LocalInferenceApi
storageUpgrade: {
getProgress: () => Promise<import('./assistant-storage-contracts').AssistantStorageProgress>
act: (action: 'retry' | 'quit') => Promise<void>
@@ -1907,6 +1910,7 @@ export type DesktopApi = {
}
updates?: {
getSettings: () => Promise<ApplicationSettings>
onSettingsChanged: (listener: (settings: ApplicationSettings) => void) => () => void
updateSettings: (
input: ApplicationSettingsUpdate
) => Promise<ApplicationSettings>
@@ -2107,6 +2111,7 @@ export type DesktopApi = {
}
tasks: {
list: () => Promise<AssistantTask[]>
getExecutionStats: (input: ExecutionStatsInput) => Promise<ExecutionStats>
setStatus: (
taskId: string,
status: Extract<AssistantTask['status'], 'completed' | 'cancelled'>
@@ -15,6 +15,7 @@ import {
goodbuddyConfigTools
} from './goodbuddy-config-tools'
import { defaultLocalToolEnvironmentSettings } from './local-tool-environment-contracts'
import { defaultApplicationNavigation } from './application-settings-contracts'
describe('GoodBuddy configuration contracts', () => {
it('publishes one valid generated example for every operation', () => {
@@ -137,6 +138,8 @@ describe('GoodBuddy configuration contracts', () => {
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
applicationNavigation: defaultApplicationNavigation,
localInferenceEnabled: true,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
+6
View File
@@ -1,4 +1,8 @@
export const ipcChannels = {
localInferenceGet: 'local-inference:get',
localInferenceAct: 'local-inference:act',
localInferenceCancel: 'local-inference:cancel',
localInferenceOpenSettings: 'local-inference:open-settings',
storageUpgradeProgress: 'app:storage-upgrade:progress',
storageUpgradeAction: 'app:storage-upgrade:action',
externalInstancesList: 'knowledge:external-instances:list',
@@ -111,6 +115,7 @@ export const ipcChannels = {
remoteChannelActivity: 'channels:remote-activity',
applicationSettingsGet: 'settings:application:get',
applicationSettingsUpdate: 'settings:application:update',
applicationSettingsChanged: 'settings:application:changed',
localToolEnvironmentGet: 'settings:local-tools:get',
localToolEnvironmentUpdate: 'settings:local-tools:update',
localToolEnvironmentRefresh: 'settings:local-tools:refresh',
@@ -213,6 +218,7 @@ export const ipcChannels = {
workspaceFileRead: 'workspace:file:read',
workspacePathOpen: 'workspace:path:open',
tasksList: 'tasks:list',
tasksExecutionStats: 'tasks:execution-stats',
tasksSetStatus: 'tasks:set-status',
activityHistoryGet: 'activity-history:get',
activityHistoryReplace: 'activity-history:replace',
+56
View File
@@ -0,0 +1,56 @@
import { z } from 'zod'
export const inferenceServiceIdSchema = z.enum(['asr', 'ocr', 'embedding', 'tts'])
export const inferenceActionSchema = z.object({
serviceId: inferenceServiceIdSchema,
action: z.enum(['start', 'stop', 'restart']),
confirmedTaskIds: z.array(z.string().max(200)).max(256).default([])
}).strict()
export const inferenceCancelSchema = z.object({ taskId: z.string().min(1).max(200) }).strict()
export type InferenceServiceId = z.infer<typeof inferenceServiceIdSchema>
export type InferenceAction = z.infer<typeof inferenceActionSchema>
export type InferenceTask = {
id: string
serviceId: InferenceServiceId
source: string
model?: string
state: 'running' | 'cancelling' | 'completed' | 'cancelled' | 'failed'
startedAt: number
finishedAt?: number
error?: string
cancelUnavailableReason?: string
}
export type InferenceResources =
| { scope: 'unavailable'; reason: string }
| {
scope: 'service-process'
pid: number
sampledAt: number
cpuPercent?: number
cpuUnavailableReason?: string
workingSetBytes?: number
memoryUnavailableReason?: string
}
export type InferenceService = {
id: InferenceServiceId
name: string
engine: string
ownership: 'request-worker' | 'renderer-worker' | 'managed-process' | 'unavailable'
state: 'idle' | 'running' | 'starting' | 'stopping' | 'stopped' | 'error' | 'unavailable' | 'unknown'
model?: string
detail: string
error?: string
actions: InferenceAction['action'][]
resources?: InferenceResources
}
export type LocalInferenceSnapshot = {
services: InferenceService[]
tasks: InferenceTask[]
externalConnections?: { id: string; name: string; model: string }[]
}
export type LocalInferenceApi = {
openSettings: () => Promise<void>
getSnapshot: () => Promise<LocalInferenceSnapshot>
act: (input: InferenceAction) => Promise<void>
cancel: (taskId: string) => Promise<void>
}