release: ship v1.5.0 with on-device writing suggestions
Adds next-sentence suggestions while typing, weekly input insights and a personal phrase memory to the desktop app, and fixes custom instructions so they process the text instead of inserting the instruction's own wording. Local model requests are now bounded and individually cancellable. Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the landing and web download links, and records the new INPUT feature rows and the open verification gaps in the infrastructure map.
This commit is contained in:
parent
99f06c253c
commit
5c11ee2fde
104 changed files with 14410 additions and 174 deletions
|
|
@ -29,6 +29,8 @@ import { registerCloudSyncHandlers } from './cloud-sync-handlers'
|
|||
import { registerAdsHandlers } from './ads-handlers'
|
||||
import { registerSupportHandlers } from './support-handlers'
|
||||
import { registerPaymentHandlers } from './payment-handlers'
|
||||
import { registerInputTelemetryHandlers } from './input-telemetry-handlers'
|
||||
import { registerSuggestionHandlers } from './suggestion-handlers'
|
||||
import { getLogger } from '../services/LoggerService'
|
||||
|
||||
const logger = getLogger('ipc')
|
||||
|
|
@ -63,5 +65,7 @@ export function registerAllIpcHandlers(): void {
|
|||
registerAdsHandlers()
|
||||
registerSupportHandlers()
|
||||
registerPaymentHandlers()
|
||||
registerInputTelemetryHandlers()
|
||||
registerSuggestionHandlers()
|
||||
logger.info('All IPC handlers registered')
|
||||
}
|
||||
|
|
|
|||
164
apps/desktop/src/main/ipc/input-telemetry-handlers.ts
Normal file
164
apps/desktop/src/main/ipc/input-telemetry-handlers.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// src/main/ipc/input-telemetry-handlers.ts
|
||||
// 입력 텔레메트리 수집 동의 · 리포트 · 개인 문구 관리.
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/errors'
|
||||
import { getInputTelemetryService } from '../services/InputTelemetryService'
|
||||
import { getPersonalGraphService } from '../services/PersonalGraphService'
|
||||
import { getSuggestionService } from '../services/SuggestionService'
|
||||
|
||||
export interface SetInputTelemetryEnabledParams {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface SetInputTelemetryPausedParams {
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
export interface InputSummaryParams {
|
||||
days?: number
|
||||
}
|
||||
|
||||
export interface InputPhrasesParams {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface DeletePhraseParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
export function registerInputTelemetryHandlers(): void {
|
||||
ipcMain.handle(IPC_CHANNELS.INPUT_TELEMETRY.GET_STATE, async () => {
|
||||
try {
|
||||
return ipcSuccess(getInputTelemetryService().getState())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.InputTelemetryConfigFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INPUT_TELEMETRY.SET_ENABLED,
|
||||
async (_event, params: SetInputTelemetryEnabledParams) => {
|
||||
try {
|
||||
getInputTelemetryService().setEnabled(params.enabled === true)
|
||||
// 동의를 끄면 제안도 더 이상 입력 맥락을 받을 수 없다.
|
||||
if (params.enabled !== true) getSuggestionService().applyConfig()
|
||||
return ipcSuccess(getInputTelemetryService().getState())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.InputTelemetryConfigFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INPUT_TELEMETRY.SET_PAUSED,
|
||||
async (_event, params: SetInputTelemetryPausedParams) => {
|
||||
try {
|
||||
getInputTelemetryService().setPaused(params.paused === true)
|
||||
return ipcSuccess(getInputTelemetryService().getState())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.InputTelemetryConfigFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INPUT_TELEMETRY.GET_SUMMARY,
|
||||
async (_event, params?: InputSummaryParams) => {
|
||||
try {
|
||||
return ipcSuccess(getInputTelemetryService().getSummary(params?.days ?? 7))
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBQueryFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.INPUT_TELEMETRY.GET_PRIVACY_RECEIPT, async () => {
|
||||
try {
|
||||
return ipcSuccess(getInputTelemetryService().getPrivacyReceipt())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBQueryFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INPUT_TELEMETRY.GET_PHRASES,
|
||||
async (_event, params?: InputPhrasesParams) => {
|
||||
try {
|
||||
return ipcSuccess(getInputTelemetryService().listPhrases(params?.limit ?? 100))
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBQueryFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INPUT_TELEMETRY.DELETE_PHRASE,
|
||||
async (_event, params: DeletePhraseParams) => {
|
||||
try {
|
||||
return ipcSuccess(getInputTelemetryService().deletePhrase(params.id))
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBWriteFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.INPUT_TELEMETRY.GET_GRAPH, async () => {
|
||||
try {
|
||||
return ipcSuccess(getPersonalGraphService().getStats())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBQueryFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.INPUT_TELEMETRY.QUERY_GRAPH,
|
||||
async (_event, params: { text: string; limit?: number }) => {
|
||||
try {
|
||||
return ipcSuccess(getPersonalGraphService().query(params.text, params.limit ?? 12))
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBQueryFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.INPUT_TELEMETRY.CLEAR_ALL, async () => {
|
||||
try {
|
||||
getInputTelemetryService().clearAll()
|
||||
return ipcSuccess(undefined)
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBWriteFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
190
apps/desktop/src/main/ipc/suggestion-handlers.ts
Normal file
190
apps/desktop/src/main/ipc/suggestion-handlers.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// src/main/ipc/suggestion-handlers.ts
|
||||
// 다음 문장 제안(ghost text) 상태/설정/수락 + 오버레이 팝업 내부 채널.
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/errors'
|
||||
import { getSuggestionService } from '../services/SuggestionService'
|
||||
import { configSet } from '../services/ConfigService'
|
||||
import { applySuggestionOverlayConfig, hideSuggestionOverlay } from '../windows/WindowManager'
|
||||
|
||||
export interface SetSuggestionConfigParams {
|
||||
enabled?: boolean
|
||||
modelId?: string | null
|
||||
triggerDelayMs?: number
|
||||
minPrefixChars?: number
|
||||
maxRequestsPerMinute?: number
|
||||
dailyBudget?: number
|
||||
overlayInteractive?: boolean
|
||||
learnTypedText?: boolean
|
||||
excludedApps?: string[]
|
||||
requestTimeoutMs?: number
|
||||
}
|
||||
|
||||
export interface SuggestionAcceptParams {
|
||||
index?: number
|
||||
}
|
||||
|
||||
export interface SuggestionHistoryParams {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export function registerSuggestionHandlers(): void {
|
||||
ipcMain.handle(IPC_CHANNELS.SUGGESTION.GET_STATE, async () => {
|
||||
try {
|
||||
return ipcSuccess(getSuggestionService().getState())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.SuggestionGenerationFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SUGGESTION.SET_CONFIG,
|
||||
async (_event, params: SetSuggestionConfigParams) => {
|
||||
try {
|
||||
const service = getSuggestionService()
|
||||
if (params.enabled !== undefined) service.setEnabled(params.enabled === true)
|
||||
if (params.modelId !== undefined) configSet('suggestionModelId', params.modelId || null)
|
||||
if (params.triggerDelayMs !== undefined) {
|
||||
configSet('suggestionTriggerDelayMs', clamp(params.triggerDelayMs, 100, 2000))
|
||||
}
|
||||
if (params.minPrefixChars !== undefined) {
|
||||
configSet('suggestionMinPrefixChars', clamp(params.minPrefixChars, 4, 40))
|
||||
}
|
||||
if (params.maxRequestsPerMinute !== undefined) {
|
||||
configSet('suggestionMaxRequestsPerMinute', clamp(params.maxRequestsPerMinute, 1, 12))
|
||||
}
|
||||
if (params.dailyBudget !== undefined) {
|
||||
configSet('suggestionDailyBudget', clamp(params.dailyBudget, 1, 100000))
|
||||
}
|
||||
if (params.requestTimeoutMs !== undefined) {
|
||||
configSet('suggestionRequestTimeoutMs', clamp(params.requestTimeoutMs, 500, 60000))
|
||||
}
|
||||
if (params.overlayInteractive !== undefined) {
|
||||
configSet('suggestionOverlayInteractive', params.overlayInteractive === true)
|
||||
applySuggestionOverlayConfig()
|
||||
}
|
||||
if (params.learnTypedText !== undefined) {
|
||||
configSet('inputLearnTypedText', params.learnTypedText === true)
|
||||
}
|
||||
if (params.excludedApps !== undefined) {
|
||||
configSet('inputExcludedApps', sanitizeExcludedApps(params.excludedApps))
|
||||
}
|
||||
|
||||
service.applyConfig()
|
||||
return ipcSuccess(service.getState())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.ConfigWriteFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SUGGESTION.REQUEST_NOW, async () => {
|
||||
try {
|
||||
const result = await getSuggestionService().requestNow()
|
||||
return ipcSuccess(result)
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.SuggestionGenerationFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SUGGESTION.ACCEPT,
|
||||
async (_event, params?: SuggestionAcceptParams) => {
|
||||
try {
|
||||
const result = await getSuggestionService().accept(params?.index)
|
||||
return ipcSuccess(result)
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.SuggestionAcceptFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SUGGESTION.NEXT, async () => {
|
||||
try {
|
||||
return ipcSuccess(getSuggestionService().next())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.SuggestionGenerationFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SUGGESTION.PREV, async () => {
|
||||
try {
|
||||
return ipcSuccess(getSuggestionService().previous())
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.SuggestionGenerationFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SUGGESTION.DISMISS, async () => {
|
||||
try {
|
||||
getSuggestionService().dismiss('dismissed')
|
||||
return ipcSuccess(undefined)
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.SuggestionGenerationFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SUGGESTION.GET_HISTORY,
|
||||
async (_event, params?: SuggestionHistoryParams) => {
|
||||
try {
|
||||
return ipcSuccess(getSuggestionService().getHistory(params?.limit ?? 50))
|
||||
} catch (error) {
|
||||
return ipcError(
|
||||
ErrorCode.DBQueryFailed,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── 오버레이 팝업 → 메인 (클릭 수락/닫기) ──────────────
|
||||
ipcMain.on(IPC_CHANNELS.POPUP_SUGGESTION.ACCEPT, (_event, index?: number) => {
|
||||
void getSuggestionService().accept(index)
|
||||
})
|
||||
|
||||
ipcMain.on(IPC_CHANNELS.POPUP_SUGGESTION.DISMISS, () => {
|
||||
hideSuggestionOverlay()
|
||||
getSuggestionService().dismiss('dismissed')
|
||||
})
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return min
|
||||
return Math.max(min, Math.min(Math.round(numeric), max))
|
||||
}
|
||||
|
||||
/** 실행 파일명만 남기고 정규화 (최대 100개). */
|
||||
function sanitizeExcludedApps(apps: string[]): string[] {
|
||||
const seen = new Set<string>()
|
||||
for (const raw of apps) {
|
||||
const value = String(raw).trim().slice(0, 120)
|
||||
if (!value) continue
|
||||
seen.add(value)
|
||||
if (seen.size >= 100) break
|
||||
}
|
||||
return [...seen]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue