feat: add app management, local inference controls, and task statistics
This commit is contained in:
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
Reference in New Issue
Block a user