[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"navigation":3,"url-settings":80,"blog-\u002Fblog\u002Fproduction-ready-productivity-app":589,"blog-author-\u002Fblog\u002Fproduction-ready-productivity-app":1250},{"id":4,"extension":5,"footer":6,"header":66,"meta":77,"stem":78,"__hash__":79},"navigation\u002Fdata\u002Fshared\u002Fnavigation.yml","yml",{"brand":7,"columns":10,"legal":56},{"name":8,"tagline":9},"Pieces","The memory layer for modern work.",[11,26,41],{"title":12,"links":13},"Product",[14,17,21,24],{"label":15,"href":16},"Pieces Desktop","\u002Fdownloads",{"label":18,"href":19,"external":20},"Pieces MCP","url:docs.mcp.overview",true,{"label":22,"href":23,"external":20},"Pieces APIs","url:docs.api",{"label":25,"href":16},"Downloads",{"title":27,"links":28},"Resources",[29,32,35,38],{"label":30,"href":31,"external":20},"Documentation","url:docs.home",{"label":33,"href":34},"Blog","\u002Fblog",{"label":36,"href":37},"Changelog","\u002Fchangelog",{"label":39,"href":40,"external":20},"GitHub","url:github.org",{"title":42,"links":43},"Company",[44,47,50,53],{"label":45,"href":46},"About","\u002Fabout",{"label":48,"href":49},"Enterprise","\u002Fenterprise",{"label":51,"href":52,"external":20},"Discord","url:social.discord",{"label":54,"href":55,"external":20},"X \u002F Twitter","url:social.x",[57,60,63],{"label":58,"href":59,"external":20},"Privacy Policy","url:legal.privacyPolicy",{"label":61,"href":62,"external":20},"Refund Policy","url:legal.refundPolicy",{"label":64,"href":65,"external":20},"Terms of Service","url:legal.terms",{"links":67,"signIn":68,"contact":71,"cta":74},[],{"label":69,"href":70},"Sign in","url:portal.home",{"label":72,"href":73},"Contact sales","url:site.contact",{"label":75,"href":76},"Download","url:routes.downloads",{},"data\u002Fshared\u002Fnavigation","Ia8tCWWqcGvuaIro8jwZ3HH-MwI66yqJpWshASJdYQ0",{"id":81,"extension":5,"links":82,"meta":586,"stem":587,"__hash__":588},"urlSettings\u002Fdata\u002Fshared\u002Furls.yml",[83,87,91,95,99,103,107,111,115,119,123,127,131,135,139,143,147,151,155,159,163,167,171,175,179,183,187,191,195,199,203,207,211,215,219,223,227,231,235,238,242,246,249,253,257,261,265,269,273,277,281,285,289,293,297,301,305,309,313,317,321,325,329,333,337,341,345,349,353,357,361,365,369,373,377,381,385,389,393,396,400,404,408,412,416,420,423,426,429,432,436,440,444,448,452,456,460,464,468,472,476,480,484,488,492,495,499,503,507,511,515,519,523,527,531,534,538,542,546,550,553,557,561,565,568,571,575,579,582],{"key":84,"label":85,"href":86},"downloads.desktop","Desktop download page","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop\u002Fdownload",{"key":88,"label":89,"href":90},"downloads.macOS.dmgArm64","macOS DMG Apple Silicon","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fpieces_for_x\u002Fdmg-arm64\u002Fdownload",{"key":92,"label":93,"href":94},"downloads.macOS.dmgIntel","macOS DMG Intel","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fpieces_for_x\u002Fdmg\u002Fdownload",{"key":96,"label":97,"href":98},"downloads.macOS.pkg","macOS PKG","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fmacos_packaging\u002Fpkg\u002Fdownload",{"key":100,"label":101,"href":102},"downloads.windows.appinstaller","Windows App Installer","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fappinstaller\u002Fpieces_for_x.appinstaller",{"key":104,"label":105,"href":106},"downloads.windows.exe","Windows EXE","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fpieces_for_x\u002Fwindows-exe\u002Fdownload",{"key":108,"label":109,"href":110},"downloads.windows.suiteManager","Windows Suite Manager","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fpieces_suite_windows\u002Fappinstaller\u002Fdownload",{"key":112,"label":113,"href":114},"downloads.linux.flatpakRepo","Linux Flatpak repository","https:\u002F\u002Fbuilds.pieces.app\u002Fpieces-flatpak-repo\u002Fpieces-flatpak.flatpakrepo",{"key":116,"label":117,"href":118},"downloads.linux.snapDesktop","Linux Snap Desktop","https:\u002F\u002Fsnapcraft.io\u002Fpieces-for-developers",{"key":120,"label":121,"href":122},"downloads.linux.snapPiecesOS","Linux Snap PiecesOS","https:\u002F\u002Fsnapcraft.io\u002Fpieces-os",{"key":124,"label":125,"href":126},"downloads.piecesOS.macOS.dmgArm64","PiecesOS macOS DMG Apple Silicon","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fos_server\u002Fdmg-arm64\u002Fdownload",{"key":128,"label":129,"href":130},"downloads.piecesOS.macOS.dmgIntel","PiecesOS macOS DMG Intel","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fos_server\u002Fdmg\u002Fdownload",{"key":132,"label":133,"href":134},"downloads.piecesOS.windows.appinstaller","PiecesOS Windows App Installer","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fappinstaller\u002Fos_server.appinstaller",{"key":136,"label":137,"href":138},"downloads.piecesOS.windows.exe","PiecesOS Windows EXE","https:\u002F\u002Fbuilds.pieces.app\u002Fstages\u002Fproduction\u002Fos_server\u002Fwindows-exe\u002Fdownload",{"key":140,"label":141,"href":142},"downloads.guides.macOS","macOS installation guide","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces\u002Fmacos-installation-guide",{"key":144,"label":145,"href":146},"downloads.guides.windows","Windows installation guide","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces\u002Fwindows-installation-guide",{"key":148,"label":149,"href":150},"downloads.guides.linux","Linux installation guide","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces\u002Flinux-installation-guide",{"key":152,"label":153,"href":154},"downloads.guides.piecesOS","PiecesOS manual installation","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fcore-dependencies\u002Fpieces-os\u002Fmanual-installation",{"key":156,"label":157,"href":158},"extensions.chrome","Chrome extension","https:\u002F\u002Fchrome.google.com\u002Fwebstore\u002Fdetail\u002Fpieces-save-code-snippets\u002Figbgibhbfonhmjlechmeefimncpekepm",{"key":160,"label":161,"href":162},"extensions.firefox","Firefox add-on","https:\u002F\u002Faddons.mozilla.org\u002Fen-US\u002Ffirefox\u002Faddon\u002Fpieces-save-code-from-the-web\u002F",{"key":164,"label":165,"href":166},"extensions.edge","Edge add-on","https:\u002F\u002Fmicrosoftedge.microsoft.com\u002Faddons\u002Fdetail\u002Fpieces-save-code-snippet\u002Fhglfimcdgonaeeobjckfdabcldfidmim",{"key":168,"label":169,"href":170},"extensions.vscode","VS Code extension","https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=MeshIntelligentTechnologiesInc.pieces-vscode",{"key":172,"label":173,"href":174},"extensions.visualStudio","Visual Studio extension","https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=MeshIntelligentTechnologiesInc.PiecesVisualStudio",{"key":176,"label":177,"href":178},"extensions.jetbrains","JetBrains plugin","https:\u002F\u002Fplugins.jetbrains.com\u002Fplugin\u002F17328-pieces--save-search-share--reuse-code-snippets",{"key":180,"label":181,"href":182},"extensions.obsidian","Obsidian plugin","https:\u002F\u002Fobsidian.md\u002Fplugins?id=pieces-for-developers",{"key":184,"label":185,"href":186},"extensions.sublime","Sublime package","https:\u002F\u002Fpackagecontrol.io\u002Fpackages\u002FPieces",{"key":188,"label":189,"href":190},"extensions.neovim","Neovim plugin","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fplugin_neo_vim",{"key":192,"label":193,"href":194},"extensions.jupyterlab","JupyterLab plugin","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fjupyterlab-pieces",{"key":196,"label":197,"href":198},"extensions.cli","Pieces CLI","https:\u002F\u002Fpypi.org\u002Fproject\u002Fpieces-cli\u002F",{"key":200,"label":201,"href":202},"docs.home","Documentation home","https:\u002F\u002Fdocs.pieces.app",{"key":204,"label":205,"href":206},"docs.getStarted","Get started docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces",{"key":208,"label":209,"href":210},"docs.api","API docs","https:\u002F\u002Fdocs.pieces.app\u002Fapi",{"key":212,"label":213,"href":214},"docs.desktop.overview","Desktop overview","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop",{"key":216,"label":217,"href":218},"docs.desktop.onboarding","Desktop onboarding","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop\u002Fonboarding",{"key":220,"label":221,"href":222},"docs.desktop.timeline","Desktop timeline docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop\u002Ftimeline",{"key":224,"label":225,"href":226},"docs.desktop.summaries","Desktop summaries docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop\u002Fsingle-click-summaries",{"key":228,"label":229,"href":230},"docs.desktop.search","Desktop conversational search docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop\u002Fconversational-search",{"key":232,"label":233,"href":234},"docs.desktop.drive","Desktop drive docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fdesktop\u002Fdrive",{"key":236,"label":237,"href":86},"docs.desktop.download","Desktop download docs",{"key":239,"label":240,"href":241},"docs.piecesOS.overview","PiecesOS overview docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fcore-dependencies",{"key":243,"label":244,"href":245},"docs.piecesOS.details","PiecesOS details docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fcore-dependencies\u002Fpieces-os",{"key":247,"label":248,"href":154},"docs.piecesOS.install","PiecesOS install docs",{"key":250,"label":251,"href":252},"docs.piecesOS.quickMenu","PiecesOS quick menu docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fcore-dependencies\u002Fpieces-os\u002Fquick-menu",{"key":254,"label":255,"href":256},"docs.piecesOS.storage","On-device storage docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fcore-dependencies\u002Fon-device-storage",{"key":258,"label":259,"href":260},"docs.piecesOS.troubleshooting","PiecesOS troubleshooting docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fcore-dependencies\u002Fpieces-os\u002Ftroubleshooting",{"key":262,"label":263,"href":264},"docs.mcp.overview","MCP overview docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp",{"key":266,"label":267,"href":268},"docs.mcp.cursor","MCP Cursor docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fcursor",{"key":270,"label":271,"href":272},"docs.mcp.vscode","MCP VS Code docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fvs-code",{"key":274,"label":275,"href":276},"docs.mcp.claudeDesktop","MCP Claude Desktop docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fclaude-desktop",{"key":278,"label":279,"href":280},"docs.mcp.claudeCode","MCP Claude Code docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fclaude-code",{"key":282,"label":283,"href":284},"docs.mcp.claudeCowork","MCP Claude Cowork docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fclaude-cowork",{"key":286,"label":287,"href":288},"docs.mcp.githubCopilot","MCP GitHub Copilot docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fgithub-copilot",{"key":290,"label":291,"href":292},"docs.mcp.goose","MCP Goose docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fgoose",{"key":294,"label":295,"href":296},"docs.mcp.windsurf","MCP Windsurf docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fwindsurf",{"key":298,"label":299,"href":300},"docs.mcp.zed","MCP Zed docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fzed",{"key":302,"label":303,"href":304},"docs.mcp.jetbrains","MCP JetBrains docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fjetbrains-ides",{"key":306,"label":307,"href":308},"docs.mcp.continueDev","MCP Continue docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fcontinue-dev",{"key":310,"label":311,"href":312},"docs.mcp.cline","MCP Cline docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fcline",{"key":314,"label":315,"href":316},"docs.mcp.raycast","MCP Raycast docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fraycast",{"key":318,"label":319,"href":320},"docs.mcp.rovoDevCli","MCP Rovo Dev CLI docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Frovo-dev-cli",{"key":322,"label":323,"href":324},"docs.mcp.openaiCodexCli","MCP OpenAI Codex CLI docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fopenai-codex-cli",{"key":326,"label":327,"href":328},"docs.mcp.googleGeminiCli","MCP Google Gemini CLI docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fgoogle-gemini-cli",{"key":330,"label":331,"href":332},"docs.mcp.amazonQ","MCP Amazon Q docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Famazon-q-developer",{"key":334,"label":335,"href":336},"docs.mcp.chatgptDev","MCP ChatGPT Developer Mode docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fchatgpt-developer-mode",{"key":338,"label":339,"href":340},"docs.mcp.openclaw","MCP OpenClaw docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fopenclaw",{"key":342,"label":343,"href":344},"docs.mcp.mcpRemote","MCP Remote docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fmcp-remote",{"key":346,"label":347,"href":348},"docs.mcp.ngrok","MCP ngrok docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmcp\u002Fngrok-setup",{"key":350,"label":351,"href":352},"docs.troubleshooting.macOS","macOS troubleshooting docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces\u002Ftroubleshooting\u002Fmacos",{"key":354,"label":355,"href":356},"docs.troubleshooting.windows","Windows troubleshooting docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces\u002Ftroubleshooting\u002Fwindows",{"key":358,"label":359,"href":360},"docs.troubleshooting.linux","Linux troubleshooting docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fmeet-pieces\u002Ftroubleshooting\u002Flinux",{"key":362,"label":363,"href":364},"docs.privacy","Privacy and security docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fprivacy-security-your-data",{"key":366,"label":367,"href":368},"docs.support","Support docs","https:\u002F\u002Fdocs.pieces.app\u002Fproducts\u002Fsupport",{"key":370,"label":371,"href":372},"portal.home","Pieces portal","https:\u002F\u002Fportal.pieces.app",{"key":374,"label":375,"href":376},"site.home","Website home","https:\u002F\u002Fpieces.app",{"key":378,"label":379,"href":380},"site.about","About page","https:\u002F\u002Fpieces.app\u002Fabout",{"key":382,"label":383,"href":384},"site.features","Features page","https:\u002F\u002Fpieces.app\u002Ffeatures",{"key":386,"label":387,"href":388},"site.plugins","Plugins page","https:\u002F\u002Fpieces.app\u002Fplugins",{"key":390,"label":391,"href":392},"site.contact","Contact page","https:\u002F\u002Fpieces.app\u002Fcontact",{"key":394,"label":36,"href":395},"site.changelog","https:\u002F\u002Fpieces.app\u002Fchangelog",{"key":397,"label":398,"href":399},"site.news","News","https:\u002F\u002Fpieces.app\u002Fnews",{"key":401,"label":402,"href":403},"site.events","Community events","https:\u002F\u002Fpieces.app\u002Fcommunity\u002Fevents",{"key":405,"label":406,"href":407},"site.userStories","User stories","https:\u002F\u002Fpieces.app\u002Fuser-stories",{"key":409,"label":410,"href":411},"site.academy","Academy","https:\u002F\u002Fpieces.app\u002Flearn\u002Facademy",{"key":413,"label":414,"href":415},"site.support","Website support","https:\u002F\u002Fpieces.app\u002Fsupport",{"key":417,"label":418,"href":419},"site.standup","Standup","https:\u002F\u002Fpieces.app\u002Fstandup",{"key":421,"label":33,"href":422},"site.blog","https:\u002F\u002Fcode.pieces.app\u002Fblog",{"key":424,"label":51,"href":425},"social.discord","https:\u002F\u002Fdiscord.gg\u002Fgetpieces",{"key":427,"label":54,"href":428},"social.x","https:\u002F\u002Fx.com\u002Fgetpieces",{"key":430,"label":431,"href":428},"social.twitter","Twitter",{"key":433,"label":434,"href":435},"social.instagram","Instagram","https:\u002F\u002Fwww.instagram.com\u002Fgetpieces\u002F",{"key":437,"label":438,"href":439},"social.tiktok","TikTok","https:\u002F\u002Fwww.tiktok.com\u002F@getpieces",{"key":441,"label":442,"href":443},"social.linkedin","LinkedIn","https:\u002F\u002Fwww.linkedin.com\u002Fcompany\u002Fgetpieces\u002F",{"key":445,"label":446,"href":447},"social.youtube","YouTube","https:\u002F\u002Fyoutube.com\u002F@getpieces",{"key":449,"label":450,"href":451},"github.org","GitHub organization","https:\u002F\u002Fgithub.com\u002Fpieces-app",{"key":453,"label":454,"href":455},"github.support","GitHub support","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fsupport",{"key":457,"label":458,"href":459},"github.issues","GitHub issues","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fsupport\u002Fissues",{"key":461,"label":462,"href":463},"github.discussions","GitHub discussions","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fsupport\u002Fdiscussions",{"key":465,"label":466,"href":467},"github.documentation","GitHub documentation","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fdocumentation",{"key":469,"label":470,"href":471},"github.opensource","GitHub open source","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fopensource",{"key":473,"label":474,"href":475},"github.sdks.python","Python SDK","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fpieces-os-client-sdk-for-python",{"key":477,"label":478,"href":479},"github.sdks.typescript","TypeScript SDK","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fpieces-os-client-sdk-for-typescript",{"key":481,"label":482,"href":483},"github.sdks.dart","Dart SDK","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fpieces-os-client-sdk-for-dart",{"key":485,"label":486,"href":487},"github.sdks.kotlin","Kotlin SDK","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fpieces-os-client-sdk-for-kotlin",{"key":489,"label":490,"href":491},"github.plugins.obsidian","Obsidian plugin repository","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fobsidian-pieces",{"key":493,"label":494,"href":194},"github.plugins.jupyterlab","JupyterLab plugin repository",{"key":496,"label":497,"href":498},"github.plugins.sublime","Sublime plugin repository","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fplugin_sublime",{"key":500,"label":501,"href":502},"github.plugins.neovim","Neovim plugin repository","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fplugin_neovim",{"key":504,"label":505,"href":506},"github.cliAgent","CLI agent repository","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fcli-agent",{"key":508,"label":509,"href":510},"github.mcpDart","MCP Dart repository","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fmcp_dart",{"key":512,"label":513,"href":514},"github.awesomePieces","Awesome Pieces repository","https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fawesome-pieces",{"key":516,"label":517,"href":518},"legal.privacyPolicy","Privacy policy","https:\u002F\u002Fpieces.app\u002Flegal\u002Fprivacy-policy",{"key":520,"label":521,"href":522},"legal.refundPolicy","Refund policy","https:\u002F\u002Fpieces.app\u002Flegal\u002Frefund-policy",{"key":524,"label":525,"href":526},"legal.terms","Terms","https:\u002F\u002Fpieces.app\u002Flegal\u002Fterms",{"key":528,"label":529,"href":530},"legal.security","Legal security","https:\u002F\u002Fpieces.app\u002Flegal\u002Fsecurity",{"key":532,"label":533,"href":447},"videos.youtubeChannel","YouTube channel",{"key":535,"label":536,"href":537},"videos.gettingStartedDesktop","Getting started desktop video","https:\u002F\u002Fyoutu.be\u002FdUr1lRM_TYk",{"key":539,"label":540,"href":541},"videos.snippetDiscovery","Snippet discovery video","https:\u002F\u002Fyoutu.be\u002FG6vb1USw-30",{"key":543,"label":544,"href":545},"sales.bookACall","Book a sales call","https:\u002F\u002Fcalendar.app.google\u002FWVUDtUfNy5Vst3sH7",{"key":547,"label":548,"href":549},"sales.enterprise","Enterprise form","https:\u002F\u002Fgetpieces.typeform.com\u002Fto\u002FaVQFTvpE",{"key":551,"label":552,"href":463},"sales.feedback","Feedback discussions",{"key":554,"label":555,"href":556},"sales.earlyAccess","Early access form","https:\u002F\u002Fgetpieces.typeform.com\u002Fearlyaccess",{"key":558,"label":559,"href":560},"sales.supportEmail","Support email","mailto:support@pieces.app",{"key":562,"label":563,"href":564},"routes.home","Home route","\u002F",{"key":566,"label":567,"href":46},"routes.about","About route",{"key":569,"label":570,"href":16},"routes.downloads","Downloads route",{"key":572,"label":573,"href":574},"routes.pricing","Pricing route","\u002Fpricing",{"key":576,"label":577,"href":578},"routes.security","Security route","\u002Fsecurity",{"key":580,"label":581,"href":49},"routes.enterprise","Enterprise route",{"key":583,"label":584,"href":585},"routes.thankYou","Thank you \u002F download route","\u002Fthank-you",{},"data\u002Fshared\u002Furls","P27xKEauu8D-8sfyr0wR4giF0teFSaCuAQ8kgcICQdI",{"id":590,"title":591,"author":592,"authorPhoto":593,"authorPhotoAlt":594,"authorSlug":595,"body":596,"buttonText":1237,"buttonUrl":1238,"category":1239,"date":1240,"description":1241,"draft":1242,"editorsPick":1242,"extension":1243,"featured":20,"image":1244,"imageAlt":594,"meta":1245,"navigation":20,"ogImage":1244,"ogImageAlt":594,"path":1246,"seo":1247,"stem":1248,"tags":594,"__hash__":1249},"blog\u002Fblog\u002Fproduction-ready-productivity-app.md","Building a daily productivity app with Pieces — Part 4: polish & production ready","Bishoy Hany","https:\u002F\u002Fstorage.googleapis.com\u002Fpieces-marketing-website\u002Fimages\u002Fblog\u002Fbuilding-pieces-productivity-with-flutter-ui\u002Fauthor.jpeg",null,"bishoy-hany",{"type":597,"value":598,"toc":1203},"minimark",[599,603,633,638,641,675,678,682,687,690,701,705,708,716,726,729,749,753,760,766,772,779,783,794,800,803,806,809,812,815,819,830,836,839,843,846,852,855,859,865,868,883,887,890,894,900,903,906,909,912,915,919,922,928,931,937,940,954,957,961,964,969,975,979,986,992,995,999,1005,1009,1015,1019,1022,1028,1031,1035,1041,1044,1050,1056,1060,1066,1072,1075,1119,1122,1126,1129,1134,1138,1144,1148,1154,1158,1161,1167,1170,1176,1179,1183,1189,1195],[600,601,602],"p",{},"Welcome to Part 4! We've come a long way:",[604,605,606,617,625],"ul",{},[607,608,609,616],"li",{},[610,611,615],"a",{"href":612,"rel":613},"https:\u002F\u002Fpieces.app\u002Fblog\u002Fbuilding-daily-standup-generator-with-pieces-api-sdk",[614],"nofollow","Part 1:"," We built a real-time sync with Pieces OS",[607,618,619,624],{},[610,620,623],{"href":621,"rel":622},"https:\u002F\u002Fpieces.app\u002Fblog\u002Fbuilding-pieces-productivity-app-with-gemini-ai",[614],"Part 2",": Then we added AI insights with Gemini",[607,626,627,632],{},[610,628,631],{"href":629,"rel":630},"https:\u002F\u002Fpieces.app\u002Fblog\u002Fbuilding-pieces-productivity-with-flutter-ui",[614],"Part 3",": Lastly, we created a beautiful Flutter UI",[634,635,637],"h2",{"id":636},"the-problems-were-solving","The problems we're solving",[600,639,640],{},"After Part 3, we had a working app, but:",[642,643,644,651,657,663,669],"ol",{},[607,645,646,650],{},[647,648,649],"strong",{},"Every restart regenerates recaps"," - Wastes API calls and time",[607,652,653,656],{},[647,654,655],{},"Errors just fail silently"," - Bad UX",[607,658,659,662],{},[647,660,661],{},"Can't regenerate a recap"," - Stuck with what you got",[607,664,665,668],{},[647,666,667],{},"No tests"," - Can't confidently make changes",[607,670,671,674],{},[647,672,673],{},"No CI\u002FCD"," - Manual testing every time",[600,676,677],{},"Let's fix all of these!",[634,679,681],{"id":680},"feature-1-persistent-caching-with-hive","Feature 1: persistent caching with Hive",[683,684,686],"h3",{"id":685},"the-problem","The problem",[600,688,689],{},"When you close the app, all those AI-generated recaps? Gone. Next time you open it, we regenerate everything. That's:",[604,691,692,695,698],{},[607,693,694],{},"Slow (3+ seconds per day)",[607,696,697],{},"Expensive (API calls cost money)",[607,699,700],{},"Wasteful (Same data, different day)",[683,702,704],{"id":703},"solution-persistent-caching-with-hive","Solution: persistent caching with Hive",[600,706,707],{},"Hive is a lightweight, fast NoSQL database for Flutter. Perfect for caching our recaps!",[600,709,710,711,715],{},"First, we need to update our model file ",[712,713,714],"code",{},"lib\u002Fmodels\u002Fdaily_recap_models.dart"," with Hive annotations:",[717,718,723],"pre",{"className":719,"code":721,"language":722},[720],"language-text","\u002F\u002F Place the import & part at the top of the Dart file\n\nimport 'package:hive\u002Fhive.dart';\n\npart 'daily_recap_models.g.dart';  \u002F\u002F Generated file\n\n\u002F\u002F Replace existing 'class DailyRecapData'. Stopping at the factory constructor.\n\n@HiveType(typeId: 0)\nclass DailyRecapData extends HiveObject {\n  @HiveField(0)\n  final DateTime date;\n\n  @HiveField(1)\n  final String summary;\n\n  @HiveField(2)\n  final List\u003CString> people;\n\n  @HiveField(3)\n  final List\u003CProjectData> projects;\n\n  @HiveField(4)\n  final List\u003CString> reminders;\n\n  @HiveField(5)\n  final List\u003CString> notes;\n\nDailyRecapData({\n    required this.date,\n    required this.summary,\n    required this.people,\n    required this.projects,\n    required this.reminders,\n    required this.notes,\n  });\n  \u002F\u002F Constructor, fromJson, toJson...\n}\n\n\u002F\u002F Replace existing 'class ProjectData'\n\n@HiveType(typeId: 1)\nclass ProjectData {\n  @HiveField(0)\n  final String name;\n\n  @HiveField(1)\n  final String description;\n\n  @HiveField(2)\n  final ProjectStatus status;\n  \n    ProjectData({\n    required this.name,\n    required this.description,\n    required this.status,\n  });\n  \n  \n}\n\n\u002F\u002F Replace the existing 'enum ProjectStatus'\n\n@HiveType(typeId: 2)\nenum ProjectStatus {\n  @HiveField(0)\n  completed,\n\n  @HiveField(1)\n  inProgress,\n\n  @HiveField(2)\n  notStarted,\n}\n","text",[712,724,721],{"__ignoreMap":725},"",[600,727,728],{},"Keep in mind:",[604,730,731,737,743],{},[607,732,733,736],{},[712,734,735],{},"@HiveType(typeId: X)"," - Unique ID for each type",[607,738,739,742],{},[712,740,741],{},"@HiveField(X)"," - Field index for serialization",[607,744,745,748],{},[712,746,747],{},"extends HiveObject"," - Makes it a Hive object",[683,750,752],{"id":751},"adding-hive-dependencies","Adding Hive Dependencies",[600,754,755,756,759],{},"Now we need to add Hive to our ",[712,757,758],{},"pubspec.yaml",":",[717,761,764],{"className":762,"code":763,"language":722},[720],"dependencies:\n  hive: ^2.2.3\n  hive_flutter: ^1.1.0\n\ndev_dependencies:\n  hive_generator: ^2.0.1\n  build_runner: ^2.4.13\n",[712,765,763],{"__ignoreMap":725},[717,767,770],{"className":768,"code":769,"language":722},[720],"flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs\n",[712,771,769],{"__ignoreMap":725},[600,773,774,775,778],{},"This generates ",[712,776,777],{},"daily_recap_models.g.dart"," with type-safe adapters!",[683,780,782],{"id":781},"updating-dailyrecapservice","Updating DailyRecapService",[600,784,785,786,789,790,793],{},"Now we need to update our ",[712,787,788],{},"DailyRecapService"," in ",[712,791,792],{},"daily_recap_service.dart"," to use Hive. Add these new methods:",[717,795,798],{"className":796,"code":797,"language":722},[720],"\u002F\u002F Add to top of file, where imports (if any) are\n\nimport 'package:hive\u002Fhive.dart';\nimport 'package:hive_flutter\u002Fhive_flutter.dart';\n\n\u002F\u002F Replace the DailyRecapService class\n\nclass DailyRecapService {\n  final GenerativeModel _model;\n  late final Box\u003CDailyRecapData> _cacheBox;  \u002F\u002F NEW: Hive cache box\n\n   DailyRecapService({required String apiKey})\n    : _model = GenerativeModel(\n        model: 'gemini-2.5-pro',\n        apiKey: apiKey,\n      );\n\n  \u002F\u002F\u002F NEW: Initialize Hive and open the cache box\n  Future\u003Cvoid> initialize() async {\n    await Hive.initFlutter();\n\n    \u002F\u002F Register adapters\n    if (!Hive.isAdapterRegistered(0)) {\n      Hive.registerAdapter(DailyRecapDataAdapter());\n    }\n    if (!Hive.isAdapterRegistered(1)) {\n      Hive.registerAdapter(ProjectDataAdapter());\n    }\n    if (!Hive.isAdapterRegistered(2)) {\n      Hive.registerAdapter(ProjectStatusAdapter());\n    }\n    \n    \u002F\u002F Open typed box\n    _cacheBox = await Hive.openBox\u003CDailyRecapData>('daily_recaps_cache');\n  }\n\n  DailyRecapData? getCachedRecap(DateTime date) {\n    final key = _dateToKey(date);\n    return _cacheBox.get(key);  \u002F\u002F Returns DailyRecapData directly!\n  }\n\n  Future\u003Cvoid> _saveToCache(DateTime date, DailyRecapData recap) async {\n    final key = _dateToKey(date);\n    await _cacheBox.put(key, recap);  \u002F\u002F Stores typed object!\n  }\n String _dateToKey(DateTime date) {\n  return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';\n}\n\n  \u002F\u002F Update the generateDailyRecap function\n\n  Future\u003CDailyRecapData> generateDailyRecap({\n    required DateTime date,\n    required List\u003CSummaryWithContent> summaries,\n    bool forceRegenerate = false, \u002F\u002F Add this field\n  }) async {\n    \u002F\u002F Check cache first (unless forcing regeneration)\n    if (!forceRegenerate) { \u002F\u002F Add this if\n      final cached = getCachedRecap(date);\n      if (cached != null) {\n        \u002F\u002F ignore: avoid_print\n        print('Using cached recap for ${_dateToKey(date)}');\n        return cached;\n      }\n    }\n\n    if (summaries.isEmpty) {\n      return DailyRecapData.empty(date);\n    }\n\n    \u002F\u002F Build context from summaries\n    final context = _buildSummariesContext(summaries);\n\n    \u002F\u002F Craft the prompt\n    final prompt = _buildPrompt(date, context);\n\n    try {\n      final response = await _model.generateContent([Content.text(prompt)]);\n      \u002F\u002F ignore: avoid_print\n      print(\"Gemini response received. ${response.text}\");\n      final jsonText = response.text ?? '{}';\n\n      \u002F\u002F Parse the JSON response\n      final data = jsonDecode(jsonText) as Map\u003CString, dynamic>;\n\n      final recap = DailyRecapData.fromJson(date, data);\n\n      \u002F\u002F Save to cache Add this as well\n      await _saveToCache(date, recap);\n\n      return recap;\n    } catch (e) {\n      \u002F\u002F ignore: avoid_print\n      print('Error generating daily recap: $e');\n      rethrow; \u002F\u002F Throw error so UI can handle it\n    }\n  }\n",[712,799,797],{"__ignoreMap":725},[600,801,802],{},"Results:",[600,804,805],{},"✅ App restart: Instant load (no API calls!)",[600,807,808],{},"✅ Type safe: No runtime errors",[600,810,811],{},"✅ Persistent: Survives app closes",[600,813,814],{},"✅ Fast: Disk read vs network call",[683,816,818],{"id":817},"updating-the-app-initialization","Updating the app initialization",[600,820,821,822,825,826,829],{},"Now in your ",[712,823,824],{},"main.dart",", update the service initialization to call the new ",[712,827,828],{},"initialize()"," method:",[717,831,834],{"className":832,"code":833,"language":722},[720],"\u002F\u002F Replace _intitializeServices()\n\nFuture\u003Cvoid> _initializeServices() async {\n  setState(() => _loadingState = LoadingState.loading);\n\n  try {\n    \u002F\u002F Initialize Pieces OS\n    await _piecesService.initialize();\n    await _piecesService.waitForInitialSync();\n\n    \u002F\u002F Initialize Gemini with Hive caching\n    const apiKey = String.fromEnvironment('GEMINI_API_KEY');\n    if (apiKey.isNotEmpty) {\n      _recapService = DailyRecapService(apiKey: apiKey);\n      await _recapService!.initialize();  \u002F\u002F Initialize Hive\n    }\n\n    \u002F\u002F Load days and recaps...\n    _loadedDays = _piecesService.getDaysWithSummaries();\n    await _loadRecapsForVisibleDays();\n\n    setState(() => _loadingState = LoadingState.healthy);\n  } catch (e) {\n    setState(() => _loadingState = LoadingState.somethingWentWrong);\n  }\n}\n",[712,835,833],{"__ignoreMap":725},[600,837,838],{},"Now, when you restart the app, cached recaps load instantly!",[634,840,842],{"id":841},"feature-2-regenerate-button","Feature 2: regenerate button",[600,844,845],{},"Sometimes you want a fresh recap (maybe you added more work, or the AI messed up). Easy fix - add a refresh button!",[717,847,850],{"className":848,"code":849,"language":722},[720],"\u002F\u002F Inside DailyRecapCard's build() method, replace the Date Header section (around line 333)\n\u002F\u002F Date Header with refresh button\n\u002F\u002F This is within children[ ... ] stopping at 'const SizedBox(height: 16)'\n\nRow(\n  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n  children: [\n    Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(dateFormatter(recap.date)),\n        Text(DateFormat('MMMM d, y').format(recap.date)),\n      ],\n    ),\n    IconButton(\n      icon: const Icon(Icons.refresh),\n      tooltip: 'Regenerate recap',\n      onPressed: onRegenerate,  \u002F\u002F Callback!\n      color: Colors.blue.shade700,\n    ),\n  ],\n)\n",[712,851,849],{"__ignoreMap":725},[600,853,854],{},"Small, unobtrusive, but super useful!",[683,856,858],{"id":857},"the-regeneration-logic","The regeneration logic",[717,860,863],{"className":861,"code":862,"language":722},[720],"\u002F\u002F Add this method after _loadMore() inside _MyHomePageState class in main.dart\n\nFuture\u003Cvoid> _regenerateRecap(DateTime day) async {\n  \u002F\u002F Mark as regenerating\n  setState(() => _regenerating.add(day));\n\n  try {\n    final summariesWithContent =\n        await _piecesService.getSummariesWithContentForDay(day);\n\n    final recap = await _recapService!.generateDailyRecap(\n      date: day,\n      summaries: summariesWithContent,\n      forceRegenerate: true,  \u002F\u002F Bypass cache!\n    );\n\n    setState(() {\n      _recapsCache[day] = recap;\n      _regenerating.remove(day);\n    });\n  } catch (e) {\n    setState(() {\n      _errorCache[day] = e.toString();\n      _regenerating.remove(day);\n    });\n  }\n}\n",[712,864,862],{"__ignoreMap":725},[600,866,867],{},"We track state:",[604,869,870,877,880],{},[607,871,872,873,876],{},"Add to ",[712,874,875],{},"_regenerating"," set → Show loading card",[607,878,879],{},"Success → Update cache and remove from set",[607,881,882],{},"Error → Add to error cache",[634,884,886],{"id":885},"feature-3-beautiful-error-handling","Feature 3: beautiful error handling",[600,888,889],{},"When Gemini fails (rate limits, network issues, invalid API key), we need good UX!",[683,891,893],{"id":892},"the-error-card","The error card",[717,895,898],{"className":896,"code":897,"language":722},[720],"\u002F\u002F Add this method after _buildLoadingCard() inside _MyHomePageState class in main.dart\n\nWidget _buildErrorCard(DateTime date, String error) {\n  return Card(\n    elevation: 4,\n    color: Colors.red.shade50,  \u002F\u002F Light red background\n    child: Padding(\n      padding: const EdgeInsets.all(16.0),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          \u002F\u002F Header with error icon\n          Row(\n            children: [\n              Icon(Icons.error_outline, color: Colors.red.shade700),\n              const SizedBox(width: 8),\n              Text(\n                dateFormatter(date),\n                style: Theme.of(context).textTheme.headlineSmall,\n              ),\n            ],\n          ),\n\n          \u002F\u002F Error message\n          Card(\n            color: Colors.white,\n            child: Padding(\n              padding: const EdgeInsets.all(16.0),\n              child: Column(\n                children: [\n                  Icon(Icons.warning, color: Colors.orange.shade700),\n                  const SizedBox(height: 8),\n                  const Text(\n                    'Failed to generate recap',\n                    style: TextStyle(fontWeight: FontWeight.bold),\n                  ),\n                  const SizedBox(height: 8),\n                  Text(\n                    'There was an issue generating the AI recap for this day.',\n                    style: TextStyle(color: Colors.grey.shade700),\n                  ),\n                  const SizedBox(height: 16),\n                  \n                  \u002F\u002F Try again button!\n                  ElevatedButton.icon(\n                    onPressed: () => _regenerateRecap(date),\n                    icon: const Icon(Icons.refresh),\n                    label: const Text('Try Again'),\n                    style: ElevatedButton.styleFrom(\n                      backgroundColor: Colors.orange,\n                      foregroundColor: Colors.white,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    ),\n  );\n}\n",[712,899,897],{"__ignoreMap":725},[600,901,902],{},"Users see:",[600,904,905],{},"❌ Clear error indication (red card)",[600,907,908],{},"⚠️ Warning icon",[600,910,911],{},"🔄 \"Try Again\" button (orange, action-oriented)",[600,913,914],{},"Much better than a crash or blank card!",[683,916,918],{"id":917},"state-management","State management",[600,920,921],{},"We track three states per card:",[717,923,926],{"className":924,"code":925,"language":722},[720],"\u002F\u002F No action needed :)\n\nfinal Map\u003CDateTime, DailyRecapData> _recapsCache = {};  \u002F\u002F Success\nfinal Map\u003CDateTime, String> _errorCache = {};           \u002F\u002F Error\nfinal Set\u003CDateTime> _regenerating = {};                 \u002F\u002F Loading\n",[712,927,925],{"__ignoreMap":725},[600,929,930],{},"Then in the UI:",[717,932,935],{"className":933,"code":934,"language":722},[720],"\u002F\u002F No action needed :)\n\nchildren: visibleDays.map((day) {\n  final recap = _recapsCache[day];\n  final error = _errorCache[day];\n  final isRegenerating = _regenerating.contains(day);\n\n  return SizedBox(\n    width: 400,\n    child: isRegenerating\n        ? _buildLoadingCard(day, isRegenerating: true)\n        : error != null\n            ? _buildErrorCard(day, error)\n            : recap != null\n                ? DailyRecapCard(recap: recap, onRegenerate: () =>\n                  _regenerateRecap(day),\n                ))\n                : _buildLoadingCard(day),\n  );\n}).toList(),\n",[712,936,934],{"__ignoreMap":725},[600,938,939],{},"Priority:",[642,941,942,945,948,951],{},[607,943,944],{},"Is it regenerating? → Show loading with \"Regenerating...\" text",[607,946,947],{},"Is there an error? → Show error card with retry button",[607,949,950],{},"Is it loaded? → Show recap card with regenerate button",[607,952,953],{},"Otherwise → Show loading card",[600,955,956],{},"Clean and predictable!",[634,958,960],{"id":959},"feature-4-automated-tests","Feature 4: automated tests",[600,962,963],{},"Writing tests for Flutter apps with external services is tricky. Here's what I did:",[600,965,966,967],{},"Start by adding our required dependencies to you ",[712,968,758],{},[717,970,973],{"className":971,"code":972,"language":722},[720],"dev_dependencies:\n  flutter_test:\n    sdk: flutter\n",[712,974,972],{"__ignoreMap":725},[683,976,978],{"id":977},"data-model-tests","Data model tests",[600,980,981,982,985],{},"Now, let’s test the core data structures. In a new Dart file ",[712,983,984],{},"test\u002Fdaily_recap_models_test.dart",", place our test:",[717,987,990],{"className":988,"code":989,"language":722},[720],"import 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:blog_walkthrough\u002Fmodels\u002Fdaily_recap_models.dart';\n\nvoid main() {\n  test('fromJson creates valid DailyRecapData object', () {\n    final date = DateTime(2025, 11, 4);\n    final json = {\n      'summary': 'Test summary',\n      'people': ['Alice', 'Bob'],\n      'projects': [\n        {\n          'name': 'Project A',\n          'description': 'Test project',\n          'status': 'completed',\n        }\n      ],\n      'reminders': ['Test reminder'],\n      'notes': ['Test note'],\n    };\n\n    final recap = DailyRecapData.fromJson(date, json);\n\n    expect(recap.summary, 'Test summary');\n    expect(recap.people, ['Alice', 'Bob']);\n    expect(recap.projects.first.status, ProjectStatus.completed);\n  });\n}\n",[712,991,989],{"__ignoreMap":725},[600,993,994],{},"This code tests all the parsing logic!",[683,996,998],{"id":997},"project-status-parsing","Project status parsing",[717,1000,1003],{"className":1001,"code":1002,"language":722},[720],"import 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:blog_walkthrough\u002Fmodels\u002Fdaily_recap_models.dart';\n\nvoid main() {\n  group('ProjectData', () {\n    test('fromJson handles all status types', () {\n      expect(\n        ProjectData.fromJson({\n          'name': 'Test Project',\n          'description': 'Test description',\n          'status': 'completed',\n        }).status,\n        ProjectStatus.completed,\n      );\n\n      expect(\n        ProjectData.fromJson({\n          'name': 'Test Project',\n          'description': 'Test description',\n          'status': 'in_progress',\n        }).status,\n        ProjectStatus.inProgress,\n      );\n\n      expect(\n        ProjectData.fromJson({\n          'name': 'Test Project',\n          'description': 'Test description',\n          'status': 'invalid',\n        }).status,\n        ProjectStatus.notStarted,  \u002F\u002F Default fallback\n      );\n    });\n  });\n}\n",[712,1004,1002],{"__ignoreMap":725},[683,1006,1008],{"id":1007},"logic-tests-no-external-dependencies","Logic tests (no external dependencies)",[717,1010,1013],{"className":1011,"code":1012,"language":722},[720],"import 'package:flutter_test\u002Fflutter_test.dart';\n\nvoid main() {\n  test('date normalization to midnight works correctly', () {\n    final date1 = DateTime(2025, 11, 4, 14, 30);  \u002F\u002F 2:30 PM\n    final date2 = DateTime(2025, 11, 4, 0, 0);    \u002F\u002F Midnight\n\n    final normalized1 = DateTime(date1.year, date1.month, date1.day);\n    final normalized2 = DateTime(date2.year, date2.month, date2.day);\n\n    expect(normalized1, normalized2);\n    expect(normalized1.hour, 0);\n  });\n}\n",[712,1014,1012],{"__ignoreMap":725},[683,1016,1018],{"id":1017},"skipping-integration-tests","Skipping integration tests",[600,1020,1021],{},"Tests that need Pieces OS running:",[717,1023,1026],{"className":1024,"code":1025,"language":722},[720],"import 'package:flutter_test\u002Fflutter_test.dart';\n\u002F\u002F import 'package:blog_walkthrough\u002Fservices\u002Fpieces_os_service.dart';\n\nvoid main() {\n  group('PiecesOSService', () {\n    test('getDaysWithSummaries requires Pieces OS', () {\n      \u002F\u002F This test requires a running Pieces OS instance\n      \u002F\u002F It's covered by integration tests\n    }, skip: 'Requires Pieces OS connection');\n  });\n}\n",[712,1027,1025],{"__ignoreMap":725},[600,1029,1030],{},"Clean and honest!",[683,1032,1034],{"id":1033},"running-the-tests","Running the tests",[717,1036,1039],{"className":1037,"code":1038,"language":722},[720],"flutter test\n",[712,1040,1038],{"__ignoreMap":725},[600,1042,1043],{},"Output:",[717,1045,1048],{"className":1046,"code":1047,"language":722},[720],"00:01 +11 ~1: All tests passed!\n",[712,1049,1047],{"__ignoreMap":725},[600,1051,1052,1055],{},[647,1053,1054],{},"11 tests passed, 1 skipped!"," ✅",[634,1057,1059],{"id":1058},"feature-5-cicd-with-github-actions","Feature 5: CI\u002FCD with GitHub Actions",[600,1061,1062,1063,759],{},"Automated testing on every push\u002FPR! Created ",[712,1064,1065],{},".github\u002Fworkflows\u002Ftest.yml",[717,1067,1070],{"className":1068,"code":1069,"language":722},[720],"name: Run Tests\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions\u002Fcheckout@v4\n\n      - name: Setup Flutter\n        uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.0'\n          channel: 'stable'\n\n      - name: Install dependencies\n        run: flutter pub get\n\n      - name: Verify formatting\n        run: dart format --set-exit-if-changed lib test\n\n      - name: Analyze code\n        run: flutter analyze\n\n      - name: Run tests\n        run: flutter test\n\n  build:\n    runs-on: macos-latest\n    needs: test\n\n    steps:\n      - name: Checkout code\n        uses: actions\u002Fcheckout@v4\n\n      - name: Setup Flutter\n        uses: subosito\u002Fflutter-action@v2\n\n      - name: Install dependencies\n        run: flutter pub get\n\n      - name: Build macOS app\n        run: flutter build macos --release\n\n      - name: Upload macOS artifact\n        uses: actions\u002Fupload-artifact@v3\n        with:\n          name: macos-app\n          path: build\u002Fmacos\u002FBuild\u002FProducts\u002FRelease\u002Fdaily_recap_app.app\n",[712,1071,1069],{"__ignoreMap":725},[600,1073,1074],{},"What it does:",[642,1076,1077,1083,1089,1095,1101,1107,1113],{},[607,1078,1079,1082],{},[647,1080,1081],{},"On every push\u002FPR",": Runs automatically",[607,1084,1085,1088],{},[647,1086,1087],{},"Check formatting",": Ensures code style",[607,1090,1091,1094],{},[647,1092,1093],{},"Analyze",": Catches potential issues",[607,1096,1097,1100],{},[647,1098,1099],{},"Run tests",": All 11 tests must pass",[607,1102,1103,1106],{},[647,1104,1105],{},"Generate coverage",": See what's tested",[607,1108,1109,1112],{},[647,1110,1111],{},"Build macOS app",": Verify it compiles",[607,1114,1115,1118],{},[647,1116,1117],{},"Upload artifact",": Downloadable .app file",[600,1120,1121],{},"Now you can't merge broken code!",[634,1123,1125],{"id":1124},"running-everything","Running everything",[683,1127,1099],{"id":1128},"run-tests",[717,1130,1132],{"className":1131,"code":1038,"language":722},[720],[712,1133,1038],{"__ignoreMap":725},[683,1135,1137],{"id":1136},"generate-hive-adapters-if-models-change","Generate Hive adapters (if models change)",[717,1139,1142],{"className":1140,"code":1141,"language":722},[720],"flutter pub run build_runner build --delete-conflicting-outputs\n",[712,1143,1141],{"__ignoreMap":725},[683,1145,1147],{"id":1146},"run-the-app","Run the app",[717,1149,1152],{"className":1150,"code":1151,"language":722},[720],"flutter run -d macos --dart-define=GEMINI_API_KEY=your-key\n",[712,1153,1151],{"__ignoreMap":725},[683,1155,1157],{"id":1156},"see-it-in-action","See it in Action",[600,1159,1160],{},"First run:",[717,1162,1165],{"className":1163,"code":1164,"language":722},[720],"Connecting to Pieces OS... (5 seconds)\nGenerating recap with AI... (3 seconds per card)\n",[712,1166,1164],{"__ignoreMap":725},[600,1168,1169],{},"Close and reopen:",[717,1171,1174],{"className":1172,"code":1173,"language":722},[720],"Connecting to Pieces OS... (5 seconds)\nUsing cached recap for 2025-11-06  ← Instant!\nUsing cached recap for 2025-11-05  ← Instant!\nUsing cached recap for 2025-11-04  ← Instant!\n",[712,1175,1173],{"__ignoreMap":725},[600,1177,1178],{},"So much faster!",[634,1180,1182],{"id":1181},"final-project-structure","Final project structure",[717,1184,1187],{"className":1185,"code":1186,"language":722},[720],"daily_recap_app\u002F\n├── lib\u002F\n│   ├── main.dart - UI with error handling & regenerate\n│   ├── models\u002F\n│   │   ├── daily_recap_models.dart - Hive models\n│   │   └── daily_recap_models.g.dart (generated) - Type adapters\n│   └── services\u002F\n│       ├── pieces_os_service.dart\n│       └── daily_recap_service.dart - With Hive caching\n├── test\u002F\n│   ├── daily_recap_service_test.dart - 9 tests\n│   └── pieces_os_service_test.dart - 3 tests\n├── .github\u002F\n│   └── workflows\u002F\n│       └── test.yml - CI\u002FCD pipeline\n└── pubspec.yaml\n",[712,1188,1186],{"__ignoreMap":725},[600,1190,1191],{},[1192,1193,1194],"em",{},"If this helped you or you built something cool with it, I'd love to hear about it!",[600,1196,1197,1202],{},[610,1198,1201],{"href":1199,"rel":1200},"https:\u002F\u002Fgithub.com\u002Fpieces-app\u002Fblog-dart-daily-stand-up-generator",[614],"Reference GitHub"," to view the full project.",{"title":725,"searchDepth":1204,"depth":1204,"links":1205},2,[1206,1207,1215,1218,1222,1229,1230,1236],{"id":636,"depth":1204,"text":637},{"id":680,"depth":1204,"text":681,"children":1208},[1209,1211,1212,1213,1214],{"id":685,"depth":1210,"text":686},3,{"id":703,"depth":1210,"text":704},{"id":751,"depth":1210,"text":752},{"id":781,"depth":1210,"text":782},{"id":817,"depth":1210,"text":818},{"id":841,"depth":1204,"text":842,"children":1216},[1217],{"id":857,"depth":1210,"text":858},{"id":885,"depth":1204,"text":886,"children":1219},[1220,1221],{"id":892,"depth":1210,"text":893},{"id":917,"depth":1210,"text":918},{"id":959,"depth":1204,"text":960,"children":1223},[1224,1225,1226,1227,1228],{"id":977,"depth":1210,"text":978},{"id":997,"depth":1210,"text":998},{"id":1007,"depth":1210,"text":1008},{"id":1017,"depth":1210,"text":1018},{"id":1033,"depth":1210,"text":1034},{"id":1058,"depth":1204,"text":1059},{"id":1124,"depth":1204,"text":1125,"children":1231},[1232,1233,1234,1235],{"id":1128,"depth":1210,"text":1099},{"id":1136,"depth":1210,"text":1137},{"id":1146,"depth":1210,"text":1147},{"id":1156,"depth":1210,"text":1157},{"id":1181,"depth":1204,"text":1182},"Try Pieces","https:\u002F\u002Fpieces.app\u002F","AI & LLM","2025-12-09T00:00:00.000Z","Build a daily productivity app with Pieces in Part 4 of the series: polish the UX, tighten performance, fix edge cases, and make your app production-ready with deployment best practices.",false,"md","https:\u002F\u002Fstorage.googleapis.com\u002Fpieces-marketing-website\u002Fimages\u002Fblog\u002Fproduction-ready-productivity-app\u002Fhero.png",{},"\u002Fblog\u002Fproduction-ready-productivity-app",{"title":591,"description":1241},"blog\u002Fproduction-ready-productivity-app","TIYgwGCljetaL_i-GDvvMoDziR-zhl2DduQW_FVEGec",{"id":1251,"title":592,"body":1252,"description":1256,"draft":1242,"extension":1243,"meta":1259,"navigation":20,"path":1260,"photo":1261,"photoAlt":594,"seo":1262,"stem":1263,"__hash__":1264},"authors\u002Fauthors\u002Fbishoy-hany.md",{"type":597,"value":1253,"toc":1257},[1254],[600,1255,1256],{},"Bishoy Hany is a Software Engineer at Pieces, where he plays a key role in building developer-first AI experiences that run locally and respect user privacy. With a strong focus on performance, reliability, and intelligent automation, Bishoy works across the full stack to help shape the future of long-term memory and contextual AI.",{"title":725,"searchDepth":1204,"depth":1204,"links":1258},[],{},"\u002Fauthors\u002Fbishoy-hany","https:\u002F\u002Fstorage.googleapis.com\u002Fpieces-marketing-website\u002Fimages\u002Fauthors\u002Fbishoy-hany.jpg",{"title":592,"description":1256},"authors\u002Fbishoy-hany","_m1q8mqNk1I4j1V0_YJSK0zuMS48DBT4Nqw9e3PZjMM"]