TYPESCRIPT · KAPSULA 0.5.0
Run TypeScript on your phone: a personal workout log
Get one .ts file from AI, import it into Kapsula 0.5.0 and use it on Android or iPhone. Save training sets, return to your history and refine the same project while keeping its data.
Download workout.tsA prompt for your own tool ↓
Your own projects require Pro: a monthly subscription with a 7-day trial for eligible new subscribers, or a one-time lifetime purchase. Your store shows the terms and price. The built-in demo is free.
From a file to your own workout log
- Update Kapsula to 0.5.0 or later from Google Play or the App Store.
- Download workout.ts. On the home screen, tap Add project and select the file. To paste source instead, choose TypeScript (.ts) first.
- Name the project and run it. Enter a sample exercise, weight and repetitions. Wait for “Set saved on this device.”.
- Return to the project list and reopen the same project. Your set should remain in History. The example UI is in English; you can change it in the source.
- Open this project’s settings → Update code. Change only the My training log heading, save and reopen the project. Check that your saved set remains.
Check the complete workflow
Import → save a set → close and reopen → change the heading → find the same saved set.
Rest timer and system notification
Rest · 90 seconds requests a system notification. Wait for System alert scheduled., lock the phone and check delivery. To test cancellation, start another rest, tap Stop and wait past the original deadline. If permission is denied, the example shows an error; a visible countdown is not confirmation of a system alert. Delivery and sound depend on OS permissions and settings. JavaScript does not run continuously in the background; Android force-stop can prevent delivery.
Supported TypeScript
One self-contained .ts script: types, interfaces, typed functions and a UI in #app. Conversion runs on the device without a CDN. It removes TypeScript syntax, but does not perform full type checking. Download the SDK declarations below for checking your code locally.
Imports (including import type), exports, npm, JSX/TSX, React, namespaces and decorators are not supported. Source is limited to 256 Ki characters. HTML and plain JavaScript remain supported. Version 0.5.0 adds no camera, file, share, location or network APIs.
Download SDK declarations · Formats and bridge API · JavaScript (.js)
Refine the code and keep your records
Data belongs to the project. Update the existing project, preserve the kapsula-ts-workout-v1 key and keep the stored records compatible. A new project gets separate storage. Keep a working source copy before a major edit: there is no version history or automatic rollback yet. Invalid TypeScript syntax is rejected before replacement; valid syntax does not guarantee correct behavior.
If code fails, use Copy the error for your AI and paste the error and source into your own AI chat. Code is not uploaded automatically.
A prompt for your own TypeScript tool
Replace the final line with your task. To refine an existing tool, include the previous source and ask to preserve storage keys and the data format.
Complete workout.ts source
// Kapsula TypeScript prototype. One file; no imports, network or npm packages.
// SDK declarations: www/sdk/kapsula.d.ts. Keep KEY and stored format when editing.
type TrainingSet = { id: string; exercise: string; weight: number; reps: number; at: number };
type WorkoutState = { schema: 1; sets: TrainingSet[]; endAt: number | null; notificationId: number | null };
const KEY = 'kapsula-ts-workout-v1';
await kapsula.ready;
const app = document.getElementById('app');
if (!app) throw new Error('Missing Kapsula UI container.');
const style = document.createElement('style');
style.textContent = `body{margin:0;padding:20px;background:#f8faf7;color:#162b23;font:16px/1.5 system-ui}
*{box-sizing:border-box}h1{font-size:32px;line-height:1.15}p{color:#53685e}label{display:block;margin:12px 0 5px}
input,button{font:inherit;border:1px solid #cad8ce;border-radius:12px;padding:12px;min-height:46px}input{width:100%;background:white;color:inherit}
button{background:#1d6148;color:white;cursor:pointer}button:disabled{opacity:.6}button.secondary{background:white;color:#1d6148}
.row{display:flex;gap:12px}.row>*{flex:1;min-width:0}.card{background:white;border:1px solid #dce5df;border-radius:18px;padding:18px;margin:18px 0}
#remaining{font-size:42px;font-variant-numeric:tabular-nums;margin:8px 0}ul{list-style:none;padding:0}li{padding:12px 0;border-top:1px solid #e3eae5}
small{color:#53685e}#error{color:#9b2727;overflow-wrap:anywhere}:focus-visible{outline:3px solid #c68f25;outline-offset:3px}`;
document.head.append(style);
app.innerHTML = `<small>TYPESCRIPT · KAPSULA</small><h1>My training log</h1>
<p>Record a set. Rest. Come back a little stronger.</p>
<form id="set-form" class="card"><label for="exercise">Exercise</label><input id="exercise" required maxlength="80" value="Squat">
<div class="row"><div><label for="weight">Weight · kg</label><input id="weight" type="number" min="0" max="1000" step="0.5" value="20" required></div>
<div><label for="reps">Repetitions</label><input id="reps" type="number" min="1" max="1000" step="1" value="10" required></div></div>
<p><button id="save-set">Save set</button></p></form>
<section class="card"><h2>Rest between sets</h2><p id="remaining" role="timer">01:30</p><p id="rest-status" role="status"></p>
<div class="row"><button id="start-rest">Rest · 90 seconds</button><button id="stop-rest" class="secondary">Stop</button></div>
<small>System alerts depend on notification permissions and device settings.</small></section>
<section class="card"><h2>History</h2><p id="total"></p><ul id="history"></ul></section>
<p id="error" role="alert"></p><p id="saved" role="status"></p>`;
function element<T extends HTMLElement>(id: string): T {
const node = document.getElementById(id);
if (!node) throw new Error('Missing element: ' + id);
return node as T;
}
function message(error: unknown): string { return error instanceof Error ? error.message : String(error); }
function validSet(item: unknown): item is TrainingSet {
if (!item || typeof item !== 'object') return false;
const v = item as Partial<TrainingSet>;
return typeof v.id === 'string' && typeof v.exercise === 'string' &&
typeof v.weight === 'number' && Number.isFinite(v.weight) && v.weight >= 0 &&
typeof v.reps === 'number' && Number.isInteger(v.reps) && v.reps > 0 &&
typeof v.at === 'number' && Number.isFinite(v.at);
}
const raw = await kapsula.storage.get<unknown>(KEY);
const state: WorkoutState = {schema: 1, sets: [], endAt: null, notificationId: null};
if (raw !== null) {
// Refuse incompatible records rather than overwriting them with an empty history.
if (!raw || typeof raw !== 'object') throw new Error('Saved data format is not supported. Keep the previous source.');
const old = raw as Partial<WorkoutState>;
if (old.schema !== 1 || !Array.isArray(old.sets) || !old.sets.every(validSet) ||
!(old.endAt === null || typeof old.endAt === 'number' && Number.isFinite(old.endAt)) ||
!(old.notificationId === null || typeof old.notificationId === 'number' && Number.isInteger(old.notificationId))) {
throw new Error('Saved data format is not supported. Keep the previous source.');
}
Object.assign(state, old);
}
let writes: Promise<void> = Promise.resolve();
function save(): Promise<void> {
const snapshot: WorkoutState = JSON.parse(JSON.stringify(state));
writes = writes.catch(() => {}).then(async () => { await kapsula.storage.set(KEY, snapshot); });
return writes;
}
function showHistory(): void {
element('total').textContent = state.sets.length + ' saved sets';
const list = element('history');
list.replaceChildren();
for (const set of [...state.sets].reverse().slice(0, 30)) {
const row = document.createElement('li');
const title = document.createElement('strong');
title.textContent = `${set.exercise} · ${set.weight} kg × ${set.reps}`;
const date = document.createElement('small');
date.textContent = new Date(set.at).toLocaleString();
row.append(title, document.createElement('br'), date); list.append(row);
}
}
element<HTMLFormElement>('set-form').onsubmit = async event => {
event.preventDefault();
const exercise = element<HTMLInputElement>('exercise').value.trim();
const weight = Number(element<HTMLInputElement>('weight').value);
const reps = Number(element<HTMLInputElement>('reps').value);
if (!exercise || !Number.isFinite(weight) || weight < 0 || weight > 1000 || !Number.isInteger(reps) || reps < 1 || reps > 1000) return;
const button = element<HTMLButtonElement>('save-set'); button.disabled = true;
element('error').textContent = ''; element('saved').textContent = '';
const set: TrainingSet = {id: Date.now().toString(36) + Math.random().toString(36).slice(2), exercise, weight, reps, at: Date.now()};
state.sets.push(set);
showHistory();
try { await save(); showHistory(); element('saved').textContent = 'Set saved on this device.'; }
catch (error) { element('error').textContent = 'Saving is not confirmed: ' + message(error); }
finally { button.disabled = false; }
};
let generation = 0;
let starting = false;
function updateClock(): void {
const left = state.endAt === null ? 90 : Math.max(0, Math.ceil((state.endAt - Date.now()) / 1000));
element('remaining').textContent = String(Math.floor(left / 60)).padStart(2, '0') + ':' + String(left % 60).padStart(2, '0');
element<HTMLButtonElement>('start-rest').disabled = starting || state.endAt !== null && state.endAt > Date.now();
}
element<HTMLButtonElement>('start-rest').onclick = async () => {
if (starting || state.endAt !== null && state.endAt > Date.now()) return;
const mine = ++generation;
const previousId = state.notificationId;
starting = true; state.endAt = Date.now() + 90000;
element('error').textContent = ''; element('rest-status').textContent = 'Requesting system alert…'; updateClock();
try {
if (previousId !== null) await kapsula.notify.cancel(previousId);
if (mine !== generation) return;
state.notificationId = null;
await save();
if (mine !== generation) return;
const notification = await kapsula.notify.schedule({at: state.endAt!, title: 'Rest is over', body: 'Ready for your next set?'});
if (mine !== generation) { await kapsula.notify.cancel(notification.id); return; }
state.notificationId = notification.id; await save();
if (mine === generation) element('rest-status').textContent = 'System alert scheduled.';
} catch (error) {
if (mine === generation) {
element('rest-status').textContent = 'Timer is visible here; system alert is not confirmed.';
element('error').textContent = message(error);
} else element('error').textContent = 'Could not cancel a late alert: ' + message(error);
} finally { starting = false; updateClock(); }
};
element<HTMLButtonElement>('stop-rest').onclick = async () => {
++generation;
const id = state.notificationId;
element('rest-status').textContent = 'Stopping…'; element('error').textContent = '';
// Clear the visible deadline immediately; an in-flight schedule cancels its late result.
state.endAt = null; updateClock();
try {
if (id !== null) await kapsula.notify.cancel(id);
state.notificationId = null; await save(); element('rest-status').textContent = 'Rest stopped.';
} catch (error) { element('rest-status').textContent = 'Alert cancellation is not confirmed.'; element('error').textContent = message(error); }
};
if (state.endAt !== null) element('rest-status').textContent = state.endAt > Date.now() ? 'Restored from saved deadline.' : 'Rest is over.';
showHistory(); updateClock();
setInterval(updateClock, 500);
How the example was checked
This source was used to check the TypeScript workflow. Manual QA of 0.5.0 on Android and iPhone was accepted on 17 October 2026. Detailed iPhone checks confirmed data preservation during editing and an invalid update; Android has overall user acceptance. This does not guarantee notification delivery under every phone setting.