Architecture

Placeholder Engine

Synced from github.com/CoWork-OS/CoWork-OS/docs

The placeholder engine powers the rotating prompt suggestions shown in the main input box. It progressively personalises what users see based on how well the system knows them.

Overview

When the input box is empty, a placeholder cycles every 4 seconds with a fade transition. Rather than showing random suggestions from a flat list, the engine selects prompts that are relevant to the current user's persona and context.

Cold start (new user)  -->  Persona-detected  -->  Fully personalised
      |                         |                         |
  Universal only       Weighted by persona      Goals, commitments,
  20 safe prompts      ~3:1 matched vs          recent tasks shown
                       universal ratio           first in rotation

Architecture

src/renderer/utils/placeholderEngine.ts   -- Pure logic, no React
src/renderer/components/MainContent.tsx   -- Consumer (useEffect)
src/renderer/styles/index.css             -- .cli-rotating-placeholder

The engine is lazy-imported from MainContent.tsx to keep the initial bundle small. It exports three functions and one type:

ExportPurpose
detectPersonas(signals)Score each persona from user data
buildDynamicPrompts(signals)Generate personalised prompts from goals/tasks
buildPlaceholders(personaResult, dynamicPrompts, pluginPrompts)Produce the final ordered playlist
UserSignalsType describing the input data shape

Persona Taxonomy

The engine defines 20 persona categories:

PersonaExample keywords
universal(always shown)
engineeringcode, deploy, api, test, docker, git, ci/cd
tradingstock, portfolio, earnings, dcf, options, backtest
educationlesson, curriculum, rubric, quiz, student, grading
marketingcampaign, seo, funnel, conversion, ad copy, content
designfigma, wireframe, wcag, typography, component
productprd, roadmap, backlog, sprint, user story, okr
founderstartup, pitch, fundraise, term sheet, runway, tam
salesprospect, pipeline, rfp, demo, crm, outreach
hrhire, recruit, onboarding, performance review, dei
legalcontract, compliance, gdpr, nda, privacy policy
datasql, dashboard, etl, tableau, anomaly, forecast
researchpaper, hypothesis, literature review, methodology
operationssop, supply chain, vendor, capacity, postmortem
supportticket, knowledge base, escalation, csat, sla
personaltravel, meal plan, budget, fitness, reading list
healthcarepatient, clinical, treatment, ehr, care plan
realestateproperty, listing, mortgage, comps, cap rate
creativevideo, podcast, storyboard, script, thumbnail
writingblog, article, draft, proofread, newsletter

Tagged Placeholder Pool

Each placeholder is tagged with one or more personas:

interface TaggedPlaceholder {
  text: string;
  personas: Persona[];
}

Examples:

{ text: "Backtest this moving-average crossover strategy", personas: ["trading"] }
{ text: "Build a competitive feature matrix",            personas: ["product", "founder"] }
{ text: "Summarize this PDF and extract the action items", personas: ["universal"] }

The pool contains ~160 entries across all 20 personas. Entries tagged "universal" are safe for any user. Many entries are cross-tagged (e.g. a DCF model is both trading and founder).

Persona Detection

Signal sources

The engine collects five signal sources in parallel on mount:

#SourceAPIWeight
1User profile factsgetUserProfile()1x (goal/work facts get 3x)
2Recent completed taskslistActivities({ activityType: "task_completed", limit: 15 })1x
3Top skills usedgetUsageInsights(workspaceId, 30).topSkills1x
4Plugin pack promptslistPluginPacks()1x
5Open commitmentsgetOpenCommitments(5)1x

Scoring algorithm

All signal text is combined into a single lowercase corpus. Each persona's keyword list is matched against the corpus:

For each persona (except universal):
  For each keyword in SIGNAL_KEYWORDS[persona]:
    if keyword found in corpus:
      score[persona] += 1

For each profile fact with category "goal" or "work":
  For each keyword match:
    score[persona] += 2   // extra weight for explicit user statements

The hasSignal flag is set to true when the total score across all personas is >= 3.

Example

A user whose profile contains goal: "ship v2 of our SaaS by March" and recent tasks like "Write unit tests for auth", "Review PR #482":

  • engineering scores high (test, PR, auth)
  • product gets a boost (ship, SaaS)
  • trading, education, etc. score 0

Result: the user sees engineering + product placeholders, with universal ones mixed in.

Progressive Tiers

Tier 1: Cold start (hasSignal === false)

New user, no profile, no tasks. Only the 20 "universal" placeholders are shown, shuffled randomly. These are domain-agnostic prompts like:

  • "Summarize this PDF and extract the action items"
  • "Help me plan my week"
  • "What's on my calendar for tomorrow?"
  • "Create a checklist for this project"

Tier 2: Persona-detected (hasSignal === true)

The top 5 scoring personas are selected. Placeholders are interleaved:

[3 matched-persona placeholders] [1 universal] [3 matched] [1 universal] ...

Non-matching persona placeholders are excluded entirely -- a trader never sees "Create a grading rubric" and a teacher never sees "Backtest this moving-average strategy".

Tier 3: Fully personalised

Dynamic prompts are generated from the user's own data and placed first in the rotation:

SourcePrompt template
Profile goalsHelp me make progress on: {goal}
Profile work factsAnything new I should know about {work}?
Open commitmentsFollow up on: {commitment}
Recent tasksContinue from: {task title}

These are deduplicated against the static pool to avoid repeats.

Rotation & Animation

The placeholder cycles every 4 seconds with a 300ms CSS fade transition:

  1. placeholderFading set to true → opacity transitions to 0
  2. After 300ms, index advances, placeholderFading set to false → opacity transitions back
  3. Cycling pauses automatically when the user has typed anything (inputValue is non-empty)

The placeholder is rendered as a custom overlay <span> (not the native placeholder attribute) so the fade animation can be controlled via CSS.

.cli-rotating-placeholder {
  color: var(--color-text-muted);
  transition: opacity 0.3s ease;
}
.cli-rotating-placeholder.fading {
  opacity: 0;
}

Adding New Placeholders

To add placeholders, edit the POOL array in placeholderEngine.ts:

{ text: "Your new placeholder text", personas: ["persona1", "persona2"] }

Guidelines:

  • Keep text under ~60 characters so it fits the input box
  • Tag with "universal" only if it makes sense for every user
  • Cross-tag when relevant (e.g. financial model → ["trading", "founder"])
  • Use natural, conversational phrasing (as if the user is typing it)

Adding New Personas

  1. Add the persona to the Persona union type
  2. Add keywords to SIGNAL_KEYWORDS
  3. Add tagged placeholders to POOL

The detection and selection logic automatically picks up new personas -- no other changes needed.